diff --git a/.cloudbuild/library_generation/cloudbuild-library-generation-push.yaml b/.cloudbuild/library_generation/cloudbuild-library-generation-push.yaml index 9c7476a33c..cd378ffaba 100644 --- a/.cloudbuild/library_generation/cloudbuild-library-generation-push.yaml +++ b/.cloudbuild/library_generation/cloudbuild-library-generation-push.yaml @@ -14,7 +14,7 @@ timeout: 7200s # 2 hours substitutions: - _GAPIC_GENERATOR_JAVA_VERSION: '2.68.1-SNAPSHOT' # {x-version-update:gapic-generator-java:current} + _GAPIC_GENERATOR_JAVA_VERSION: '2.68.1' # {x-version-update:gapic-generator-java:current} _PRIVATE_IMAGE_NAME: "us-docker.pkg.dev/java-hermetic-build-prod/private-resources/java-library-generation" _PRIVATE_SHA_IMAGE_ID: "${_PRIVATE_IMAGE_NAME}:${COMMIT_SHA}" _PRIVATE_LATEST_IMAGE_ID: "${_PRIVATE_IMAGE_NAME}:latest" diff --git a/.cloudbuild/library_generation/library_generation.Dockerfile b/.cloudbuild/library_generation/library_generation.Dockerfile index f265c74393..32044ad919 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 -T 1.5C -DskipTests -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip +RUN mvn install -B -ntp -T 1.5C -DskipTests -Dcheckstyle.skip -Dclirr.skip -Denforcer.skip -Dfmt.skip -pl gapic-generator-java --also-make 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" @@ -38,7 +38,7 @@ FROM docker.io/library/python:3.13.2-slim@sha256:6b3223eb4d93718828223966ad31690 ARG OWLBOT_CLI_COMMITTISH=3a68a9c0de318784b3aefadcc502a6521b3f1bc5 ARG PROTOC_VERSION=33.2 -ARG GRPC_VERSION=1.76.3 +ARG GRPC_VERSION=1.80.0 ENV HOME=/home ENV OS_ARCHITECTURE="linux-x86_64" diff --git a/.cloudbuild/library_generation/library_generation_airlock.Dockerfile b/.cloudbuild/library_generation/library_generation_airlock.Dockerfile index 1d6ddbbb0e..59062b8bd6 100644 --- a/.cloudbuild/library_generation/library_generation_airlock.Dockerfile +++ b/.cloudbuild/library_generation/library_generation_airlock.Dockerfile @@ -39,7 +39,7 @@ FROM us-docker.pkg.dev/artifact-foundry-prod/docker-3p-trusted/python@sha256:afc ARG OWLBOT_CLI_COMMITTISH=3a68a9c0de318784b3aefadcc502a6521b3f1bc5 ARG PROTOC_VERSION=33.2 -ARG GRPC_VERSION=1.76.3 +ARG GRPC_VERSION=1.80.0 ENV HOME=/home ENV OS_ARCHITECTURE="linux-x86_64" diff --git a/.github/scripts/action.yaml b/.github/scripts/action.yaml index f67f2a5229..7546ec0e1f 100644 --- a/.github/scripts/action.yaml +++ b/.github/scripts/action.yaml @@ -36,6 +36,9 @@ inputs: token: description: Personal Access Token required: true + force_regenerate_all: + description: true if we want to regenerate all libraries + required: false runs: using: "composite" @@ -76,10 +79,12 @@ runs: --target_branch "${BASE_REF}" \ --current_branch "${HEAD_REF}" \ --showcase_mode "${SHOWCASE_MODE}" \ - --image_tag "${IMAGE_TAG}" + --image_tag "${IMAGE_TAG}" \ + --force_regenerate_all "${FORCE_REGENERATE_ALL}" env: BASE_REF: ${{ inputs.base_ref }} HEAD_REF: ${{ inputs.head_ref }} IMAGE_TAG: ${{ inputs.image_tag }} SHOWCASE_MODE: ${{ inputs.showcase_mode }} GH_TOKEN: ${{ inputs.token }} + FORCE_REGENERATE_ALL: ${{ inputs.force_regenerate_all }} diff --git a/.github/scripts/hermetic_library_generation.sh b/.github/scripts/hermetic_library_generation.sh index 5da0e64c3a..fc991aa3e9 100755 --- a/.github/scripts/hermetic_library_generation.sh +++ b/.github/scripts/hermetic_library_generation.sh @@ -50,6 +50,10 @@ case "${key}" in showcase_mode="$2" shift ;; + --force_regenerate_all) + force_regenerate_all="$2" + shift + ;; *) echo "Invalid option: [$1]" exit 1 @@ -81,6 +85,10 @@ if [ -z "${image_tag}" ]; then image_tag=$(grep "gapic_generator_version" "${generation_config}" | cut -d ':' -f 2 | xargs) fi +if [ -z "${force_regenerate_all}" ]; then + force_regenerate_all="false" +fi + workspace_name="/workspace" baseline_generation_config="baseline_generation_config.yaml" message="chore: generate libraries at $(date)" @@ -109,7 +117,8 @@ fi changed_libraries_file="$(mktemp)" python hermetic_build/common/cli/get_changed_libraries.py create \ --baseline-generation-config-path="${baseline_generation_config}" \ - --current-generation-config-path="${generation_config}" | tee "${changed_libraries_file}" + --current-generation-config-path="${generation_config}"\ + --force-regenerate-all="${force_regenerate_all}" | tee "${changed_libraries_file}" changed_libraries="$(cat "${changed_libraries_file}")" echo "Changed libraries are: ${changed_libraries:-"No changed library"}." diff --git a/.kokoro/presubmit/common_test.sh b/.kokoro/presubmit/common_test.sh deleted file mode 100755 index fcc9638eb7..0000000000 --- a/.kokoro/presubmit/common_test.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -scriptDir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") -cd "${scriptDir}/../.." # cd to the root of this repo -source "$scriptDir/common.sh" -mkdir -p target -cd target - -function test_find_all_poms_with_versioned_dependency { - mkdir -p test_find_all_poms_with_dependency - pushd test_find_all_poms_with_dependency - cp ../../java-showcase/gapic-showcase/pom.xml pom.xml - - find_all_poms_with_versioned_dependency 'truth' - if [ "${#POMS[@]}" != 1 ]; then - echo 'find_all_poms_with_versioned_dependency did not find the expected pom' - exit 1 - elif [ "${POMS[0]}" != './pom.xml' ]; then - echo "find_all_poms_with_versioned_dependency found ${POMS[0]} instead of expected ./pom.xml" - exit 1 - fi - - find_all_poms_with_versioned_dependency 'gax-grpc' # Versioned by shared-deps - if [ "${#POMS[@]}" != 0 ]; then - echo 'find_all_poms_with_versioned_dependency found unexpected pom' - exit 1 - fi - - popd -} - -function test_update_pom_dependency { - mkdir -p test_update_pom_dependency - pushd test_update_pom_dependency - cp ../../java-showcase/gapic-showcase/pom.xml pom.xml - - update_pom_dependency . truth "99.88.77" - - xmllint --shell pom.xml &>/dev/null </dev/null; then - echo "update_pom_dependency failed to change version to expected value." - exit 1 - fi - rm found-version.txt - popd -} - -function test_parse_pom_version { - mkdir -p test_parse_pom_version - pushd test_parse_pom_version - cp ../../java-showcase/gapic-showcase/pom.xml pom.xml - - VERSION=$(parse_pom_version .) - if [ "$VERSION" != "0.0.1-SNAPSHOT" ]; then - echo "parse_pom_version failed to read expected version of gapic-showcase." - fi - popd -} - -test_find_all_poms_with_versioned_dependency -test_update_pom_dependency -test_parse_pom_version diff --git a/.kokoro/presubmit/downstream-compatibility-spring.sh b/.kokoro/presubmit/downstream-compatibility-spring.sh deleted file mode 100755 index edc99d6cb7..0000000000 --- a/.kokoro/presubmit/downstream-compatibility-spring.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This script is only meant to test spring-cloud-gpc, specifically on its -# autoconfig generation workflow - -set -eox pipefail - -# Get the directory of the build script -scriptDir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") -cd "${scriptDir}/../.." # cd to the root of this repo -source "$scriptDir/common.sh" - -install_repo_modules -GAPIC_GENERATOR_VERSION=$(parse_pom_version "gapic-generator-java-bom") -echo "Install complete. gapic-generator-java-bom = $GAPIC_GENERATOR_VERSION" - -pushd gapic-generator-java/target -# Download and configure spring-cloud-gcp for testing -last_release=$(find_last_release_version "spring-cloud-gcp" "main" "GoogleCloudPlatform") -git clone "https://github.com/GoogleCloudPlatform/spring-cloud-gcp.git" --depth=1 --branch "v$last_release" -update_all_poms_dependency "spring-cloud-gcp" "gapic-generator-java-bom" "${GAPIC_GENERATOR_VERSION}" - -# Install spring-cloud-gcp modules -pushd spring-cloud-gcp -./mvnw \ - -U \ - --batch-mode \ - --no-transfer-progress \ - --show-version \ - --threads 1.5C \ - --define maven.test.skip=true \ - --define maven.javadoc.skip=true \ - install - - -# Generate showcase autoconfig -pushd spring-cloud-generator -# The script is not executable for non-owners. Here we manually chmod it. -# TODO(diegomarquezp): remove this line after -# https://github.com/GoogleCloudPlatform/spring-cloud-gcp/pull/3183 is merged and released. -chmod 755 ./scripts/generate-showcase.sh -./scripts/generate-showcase.sh -pushd showcase/showcase-spring-starter -mvn verify -popd # showcase/showcase-spring-starter - -popd # spring-cloud-generator -popd # spring-cloud-gcp -popd # gapic-generator-java/target diff --git a/.kokoro/presubmit/downstream-compatibility.sh b/.kokoro/presubmit/downstream-compatibility.sh deleted file mode 100755 index 2e0b8c7c12..0000000000 --- a/.kokoro/presubmit/downstream-compatibility.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -eo pipefail - -# Comma-delimited list of repos to test with the local java-shared-dependencies -if [ -z "${REPOS_UNDER_TEST}" ]; then - echo "REPOS_UNDER_TEST must be set to run downstream-compatibility.sh" - exit 1 -fi - - -# Get the directory of the build script -scriptDir=$(realpath "$(dirname "${BASH_SOURCE[0]}")") -cd "${scriptDir}/../.." # cd to the root of this repo -source "$scriptDir/common.sh" - -setup_maven_mirror - -install_repo_modules '!gapic-generator-java' -SHARED_DEPS_VERSION=$(parse_pom_version java-shared-dependencies) -echo "Install complete. java-shared-dependencies = $SHARED_DEPS_VERSION" - -pushd java-shared-dependencies/target -for repo in ${REPOS_UNDER_TEST//,/ }; do # Split on comma - # Perform testing on last release, not HEAD - last_release=$(find_last_release_version "$repo") - git clone "https://github.com/googleapis/$repo.git" --depth=1 --branch "v$last_release" - update_all_poms_dependency "$repo" google-cloud-shared-dependencies "$SHARED_DEPS_VERSION" - pushd "$repo" - JOB_TYPE="test" ./.kokoro/build.sh - popd -done -popd diff --git a/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java b/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java index cc9f4c110e..d0d6d33f27 100644 --- a/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java +++ b/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java @@ -296,6 +296,129 @@ public Set vars() { return bindings.keySet(); } + /** Returns the set of resource literals. A resource literal is a literal followed by a binding */ + // For example, projects/{project} is a literal/binding pair and projects is a resource literal. + public Set getResourceLiterals() { + Set canonicalSegments = new java.util.LinkedHashSet<>(); + boolean inBinding = false; + for (int i = 0; i < segments.size(); i++) { + Segment seg = segments.get(i); + if (seg.kind() == SegmentKind.BINDING) { + inBinding = true; + } else if (seg.kind() == SegmentKind.END_BINDING) { + inBinding = false; + } else if (seg.kind() == SegmentKind.LITERAL) { + String value = seg.value(); + // Skipping version literals such as v1, v1beta1 + if (value.matches("^v\\d+[a-zA-Z0-9]*$")) { + continue; + } + if (inBinding) { + // This is for extracting "projects" and "locations" from named binding + // {name=projects/*/locations/*} + canonicalSegments.add(value); + } else if (i + 1 < segments.size() && segments.get(i + 1).kind() == SegmentKind.BINDING) { + // This is for regular cases projects/{project}/locations/{location} + canonicalSegments.add(value); + } + } + } + return canonicalSegments; + } + + /** + * Returns the canonical resource name string. A canonical resource name is extracted from the + * template by finding the version literal, then finding the last binding that is a + * literal/binding pair or named binding, and then extracting the segments between the version + * literal and the last binding (inclusive). This is a heuristic method that should only be used + * for allowlisted services. There are also known gaps, such as the fact that it does not work + * properly for singleton resources. + */ + // For example, projects/{project} is a literal/binding pair. {bar=projects/*/locations/*/bars/*} + // is a named binding. + // If a template is /compute/v1/projects/{project}/locations/{location}, known resource literals + // are "projects" and "locations", the canonical resource name would be + // projects/{project}/locations/{location}. See unit tests for all cases. + public String getCanonicalResourceName(Set knownResourceLiterals) { + if (knownResourceLiterals == null) { + return ""; + } + + int startIndex = 0; + for (int i = 0; i < segments.size(); i++) { + Segment seg = segments.get(i); + if (seg.kind() == SegmentKind.LITERAL) { + String value = seg.value(); + if (value.matches("^v\\d+[a-zA-Z0-9]*$")) { + startIndex = i + 1; + break; + } + } + } + + int lastValidEndBindingIndex = -1; + // Iterate from the end of the segments to find the last valid resource binding. + // Searching backwards allows us to stop immediately once the last valid pair is found. + for (int i = segments.size() - 1; i >= 0; i--) { + Segment seg = segments.get(i); + + // We are looking for the end of a binding (e.g., "}" in "{project}" or "{name=projects/*}") + if (seg.kind() == SegmentKind.END_BINDING) { + int bindingStartIndex = -1; + int literalCountInBinding = 0; + boolean isValidPair = false; + + // Traverse backwards to find the start of this specific binding + // and count the literals captured inside it. + for (int j = i - 1; j >= 0; j--) { + Segment innerSeg = segments.get(j); + if (innerSeg.kind() == SegmentKind.BINDING) { + bindingStartIndex = j; + break; + } else if (innerSeg.kind() == SegmentKind.LITERAL + || innerSeg.kind() == SegmentKind.PATH_WILDCARD) { + literalCountInBinding++; + } + } + + if (bindingStartIndex != -1) { + // 1. If the binding contains any literals, it is considered a valid named resource + // binding. + if (literalCountInBinding > 0) { + isValidPair = true; + } else if (bindingStartIndex > 0) { + // 2. For simple bindings like "{project}", the binding itself has no inner literal + // resources. + // Instead, we check if the literal segment immediately preceding it (e.g., "projects/") + // is a known resource. + Segment prevSeg = segments.get(bindingStartIndex - 1); + if (prevSeg.kind() == SegmentKind.LITERAL + && knownResourceLiterals.contains(prevSeg.value())) { + isValidPair = true; + } + } + + if (isValidPair) { + // We successfully found the last valid binding! Record its end index and terminate the + // search. + lastValidEndBindingIndex = i; + break; + } + // The current binding wasn't a valid resource pair. + // Skip over all inner segments of this invalid binding so we don't evaluate them again. + i = bindingStartIndex; + } + } + } + + if (lastValidEndBindingIndex == -1 || lastValidEndBindingIndex < startIndex) { + return ""; + } + + List canonicalSegments = segments.subList(startIndex, lastValidEndBindingIndex + 1); + return toSyntax(canonicalSegments, true).replace("=*}", "}"); + } + /** * Returns a template for the parent of this template. * diff --git a/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java b/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java index 6ac45cdf20..db23c9bc8a 100644 --- a/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java +++ b/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java @@ -31,7 +31,9 @@ package com.google.api.pathtemplate; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.truth.Truth; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.stream.Stream; @@ -894,6 +896,170 @@ void testTemplateWithMultipleSimpleBindings() { Truth.assertThat(url).isEqualTo("v1/shelves/s1/books/b1"); } + @Test + void testGetResourceLiterals_simplePath() { + PathTemplate template = + PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}"); + Truth.assertThat(template.getResourceLiterals()) + .containsExactly("projects", "locations", "widgets"); + } + + @Test + void testGetResourceLiterals_multipleLiterals() { + PathTemplate template = + PathTemplate.create( + "/compute/v1/projects/{project}/global/locations/{location}/widgets/{widget}"); + Truth.assertThat(template.getResourceLiterals()) + .containsExactly("projects", "locations", "widgets"); + } + + @Test + void testGetResourceLiterals_regexPath() { + PathTemplate template = + PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}"); + Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances"); + } + + @Test + void testGetResourceLiterals_onlyNonResourceLiterals() { + PathTemplate template = PathTemplate.create("compute/v1/projects"); + Truth.assertThat(template.getResourceLiterals()).isEmpty(); + } + + @Test + void testGetResourceLiterals_nameBinding() { + PathTemplate template = PathTemplate.create("v1/{name=projects/*/instances/*}"); + Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances"); + } + + @Test + void testGetResourceLiterals_complexResourceId() { + PathTemplate template = PathTemplate.create("projects/{project}/zones/{zone_a}~{zone_b}"); + Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "zones"); + } + + @Test + void testGetResourceLiterals_customVerb() { + PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute"); + Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances"); + } + + @Test + void testGetCanonicalResourceName_namedBindingsSimple() { + Set moreKnownResources = ImmutableSet.of("projects", "locations", "bars"); + PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/bars/*}"); + Truth.assertThat(template.getCanonicalResourceName(moreKnownResources)) + .isEqualTo("{bar=projects/*/locations/*/bars/*}"); + } + + @Test + void testGetCanonicalResourceName_namedBindingsWithUnknownResource() { + Set knownResources = ImmutableSet.of(); + PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/unknown/*}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("{bar=projects/*/locations/*/unknown/*}"); + } + + @Test + void testGetCanonicalResourceName_simplePath() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = + PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("projects/{project}/locations/{location}/widgets/{widget}"); + } + + @Test + void testGetCanonicalResourceName_v1beta1WithSimplePath() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = + PathTemplate.create( + "/compute/v1beta1/projects/{project}/locations/{location}/widgets/{widget}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("projects/{project}/locations/{location}/widgets/{widget}"); + } + + @Test + void testGetCanonicalResourceName_regexVariables() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = + PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("projects/{project=projects/*}/instances/{instance_id=instances/*}"); + } + + @Test + void testGetCanonicalResourceName_noVariables() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = PathTemplate.create("v1/projects/locations"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)).isEmpty(); + } + + @Test + void testGetCanonicalResourceName_unknownResource() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = + PathTemplate.create("v1/projects/{project}/unknownResource/{unknownResource}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("projects/{project}"); + } + + @Test + void testGetCanonicalResourceName_customVerb() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("projects/{project}/instances/{instance}"); + } + + @Test + void testGetCanonicalResourceName_nameBindingMixedWithSimpleBinding() { + Set knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets"); + PathTemplate template = + PathTemplate.create("v1/{field=projects/*/instances/*}/actions/{action}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("{field=projects/*/instances/*}"); + } + + @Test + void testGetCanonicalResourceName_multipleLiteralsWithSimpleBinding() { + Set knownResources = ImmutableSet.of("actions"); + PathTemplate template = PathTemplate.create("v1/locations/global/actions/{action}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("locations/global/actions/{action}"); + } + + @Test + void testGetCanonicalResourceName_multipleLiteralsWithMultipleBindings() { + Set knownResources = ImmutableSet.of("instances", "actions"); + PathTemplate template = + PathTemplate.create("v1/locations/global/instances/{instance}/actions/{action}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("locations/global/instances/{instance}/actions/{action}"); + } + + @Test + void testGetCanonicalResourceName_multipleLiteralsBetweenMultipleBindings() { + Set knownResources = ImmutableSet.of("instances", "actions"); + PathTemplate template = + PathTemplate.create("v1/instances/{instance}/locations/global/actions/{action}"); + Truth.assertThat(template.getCanonicalResourceName(knownResources)) + .isEqualTo("instances/{instance}/locations/global/actions/{action}"); + } + + @Test + void testGetCanonicalResourceName_nullKnownResources() { + PathTemplate template = + PathTemplate.create("v1/projects/{project}/locations/{location}/widgets/{widget}"); + Truth.assertThat(template.getCanonicalResourceName(null)).isEmpty(); + } + + @Test + void testGetCanonicalResourceName_pathWildCard() { + PathTemplate template = PathTemplate.create("/v1/{resource=**}:setIamPolicy"); + Truth.assertThat(template.getCanonicalResourceName(new HashSet<>())).isEqualTo("{resource=**}"); + } + private static void assertPositionalMatch(Map match, String... expected) { Truth.assertThat(match).isNotNull(); int i = 0; diff --git a/coverage-report/pom.xml b/coverage-report/pom.xml index 333cf5059d..1fd2cb64d5 100644 --- a/coverage-report/pom.xml +++ b/coverage-report/pom.xml @@ -31,22 +31,22 @@ com.google.api gax - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT com.google.api gax-grpc - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT com.google.api gax-httpjson - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT com.google.api api-common - 2.59.1-SNAPSHOT + 2.59.1-SNAPSHOT diff --git a/dependencies.txt b/dependencies.txt index 7650be92ce..0f5841f847 100644 --- a/dependencies.txt +++ b/dependencies.txt @@ -9,12 +9,12 @@ # Pom-Parent Dependencies # These dependencies are declared: https://github.com/googleapis/sdk-platform-java/blob/main/gapic-generator-java-pom-parent/pom.xml javax.annotation:javax.annotation-api,javax.annotation-api=1.3.2 -io.grpc:grpc-bom,grpc=1.79.0 +io.grpc:grpc-bom,grpc=1.80.0 com.google.auth:google-auth-library-bom,google.auth=1.43.0 com.google.http-client:google-http-client,google.http-client=2.1.0 com.google.code.gson:gson,gson=2.13.2 com.google.guava:guava,guava=33.5.0-jre -com.google.protobuf:protobuf-java,protobuf=4.34.0 +com.google.protobuf:protobuf-java,protobuf=4.34.1 io.opentelemetry:opentelemetry-bom,opentelemetry=1.60.1 com.google.errorprone:error_prone_annotations,errorprone=2.48.0 com.google.j2objc:j2objc-annotations,j2objc-annotations=3.1 @@ -32,7 +32,7 @@ com.google.api-client:google-api-client,google.api-client=2.9.0 org.threeten:threeten-extra,threeten-extra=1.8.0 io.opencensus:opencensus-api,opencensus=0.31.1 com.google.code.findbugs:jsr305,findbugs=3.0.2 -com.fasterxml.jackson:jackson-bom,jackson=2.21.1 +com.fasterxml.jackson:jackson-bom,jackson=2.21.2 commons-codec:commons-codec,codec=1.21.0 org.apache.httpcomponents:httpclient,httpcomponents.httpclient=4.5.14 org.apache.httpcomponents:httpcore,httpcomponents.httpcore=4.4.16 @@ -45,7 +45,7 @@ com.google.cloud.opentelemetry:exporter-metrics,google.cloud.opentelemetry=0.36. com.google.flogger:flogger,flogger=0.9 org.apache.arrow:arrow-memory-core,arrow=18.3.0 dev.cel:cel,dev.cel=0.12.0 -com.google.crypto.tink:tink,com.google.crypto.tink=1.20.0 +com.google.crypto.tink:tink,com.google.crypto.tink=1.21.0 # The follow opentelemetry dependencies have a different version from the opentelemetry-bom io.opentelemetry.semconv:opentelemetry-semconv,opentelemetry-semconv=1.40.0 io.opentelemetry.contrib:opentelemetry-gcp-resources,io.opentelemetry.contrib.opentelemetry-gcp-resources=1.52.0-alpha diff --git a/gapic-generator-java-bom/pom.xml b/gapic-generator-java-bom/pom.xml index b54c6a7684..ed689535ab 100644 --- a/gapic-generator-java-bom/pom.xml +++ b/gapic-generator-java-bom/pom.xml @@ -25,14 +25,14 @@ com.google.auth google-auth-library-bom - ${google.auth.version} + 1.43.0 pom import com.google.auth google-auth-library-oauth2-http - ${google.auth.version} + 1.43.0 test-jar testlib test diff --git a/gapic-generator-java-pom-parent/pom.xml b/gapic-generator-java-pom-parent/pom.xml index d77f79f9d3..3eafa24488 100644 --- a/gapic-generator-java-pom-parent/pom.xml +++ b/gapic-generator-java-pom-parent/pom.xml @@ -27,14 +27,13 @@ 1.3.2 - 1.76.3 - 1.43.0 + 1.80.0 2.1.0 2.12.1 33.5.0-jre 4.33.2 1.51.0 - 2.42.0 + 2.45.0 3.1 1.7.0 5.11.4 diff --git a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java index 98d8a121f7..922dda48dc 100644 --- a/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java +++ b/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java @@ -85,20 +85,26 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Generated; import javax.annotation.Nullable; public abstract class AbstractTransportServiceStubClassComposer implements ClassComposer { + private static final List AIP_STANDARDS_METHODS = + ImmutableList.of( + "Get", "List", "Create", "Delete", "Update", "Patch", "Insert", "AggregatedList"); private static final Statement EMPTY_LINE_STATEMENT = EmptyLineStatement.create(); private static final String METHOD_DESCRIPTOR_NAME_PATTERN = "%sMethodDescriptor"; @@ -109,6 +115,9 @@ public abstract class AbstractTransportServiceStubClassComposer implements Class protected static final String CALLABLE_CLASS_MEMBER_PATTERN = "%sCallable"; private static final String OPERATION_CALLABLE_CLASS_MEMBER_PATTERN = "%sOperationCallable"; + private static final ImmutableList HEURISTIC_ENABLED_PACKAGES = + ImmutableList.of("google.cloud.compute", "google.cloud.sql", "google.cloud.bigquery"); + protected static final TypeStore FIXED_TYPESTORE = createStaticTypes(); private final TransportContext transportContext; @@ -147,6 +156,15 @@ private static TypeStore createStaticTypes() { return new TypeStore(concreteClazzes); } + private static boolean isHeuristicEnabled(String protoPackage) { + for (String prefix : HEURISTIC_ENABLED_PACKAGES) { + if (protoPackage.startsWith(prefix)) { + return true; + } + } + return false; + } + @Override public GapicClass generate(GapicContext context, Service service) { if (!service.hasAnyEnabledMethodsForTransport(getTransportContext().transport())) { @@ -277,11 +295,13 @@ protected List createOperationsStubGetterMethod( } protected Expr createTransportSettingsInitExpr( + Service service, Method method, VariableExpr transportSettingsVarExpr, VariableExpr methodDescriptorVarExpr, List classStatements, - ImmutableMap messageTypes) { + ImmutableMap messageTypes, + Set knownResources) { MethodInvocationExpr callSettingsBuilderExpr = MethodInvocationExpr.builder() .setStaticReferenceType(getTransportContext().transportCallSettingsType()) @@ -331,7 +351,9 @@ protected Expr createTransportSettingsInitExpr( .build(); } - LambdaExpr extractor = createResourceNameExtractorClassInstance(method, messageTypes); + LambdaExpr extractor = + createResourceNameExtractorClassInstance( + service, method, messageTypes, knownResources, classStatements); if (extractor != null) { callSettingsBuilderExpr = MethodInvocationExpr.builder() @@ -774,18 +796,21 @@ protected List createConstructorMethods( .build())) .build()))); + Set knownResources = getKnownResources(service); secondCtorExprs.addAll( service.methods().stream() .filter(x -> x.isSupportedByTransport(getTransportContext().transport())) .map( m -> createTransportSettingsInitExpr( + service, m, javaStyleMethodNameToTransportSettingsVarExprs.get( JavaStyle.toLowerCamelCase(m.name())), protoMethodNameToDescriptorVarExprs.get(m.name()), classStatements, - context.messages())) + context.messages(), + knownResources)) .collect(Collectors.toList())); secondCtorStatements.addAll( secondCtorExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())); @@ -1510,29 +1535,143 @@ private static Predicate shouldAutoPopulate(Message methodRequestMessage * resource reference (see {@link Field#hasResourceReference()}) */ @Nullable - protected static LambdaExpr createResourceNameExtractorClassInstance( - Method method, ImmutableMap messageTypes) { + protected LambdaExpr createResourceNameExtractorClassInstance( + Service service, + Method method, + ImmutableMap messageTypes, + Set knownResources, + List classStatements) { Field resourceNameField = getDestinationResourceIdField(method, messageTypes); - if (resourceNameField == null) { + if (resourceNameField != null) { + // Expected expression: request -> request.getField() + VariableExpr requestVarExpr = createRequestVarExpr(method); + List bodyStatements = new ArrayList<>(); + Expr returnExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestVarExpr) + .setMethodName( + String.format("get%s", JavaStyle.toUpperCamelCase(resourceNameField.name()))) + .setReturnType(TypeNode.STRING) + .build(); + + return LambdaExpr.builder() + .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) + .setBody(bodyStatements) + .setReturnExpr(returnExpr) + .build(); + } + + if (!isHeuristicEnabled(service.protoPakkage())) { + return null; + } + + if (!method.hasHttpBindings()) { return null; } - // Expected expression: request -> request.getField() + String canonicalPath = + extractCanonicalResourceName(method.httpBindings().pattern(), knownResources); + if (!canonicalPath.contains("{")) { + return null; + } + + // Expected expression: private static final PathTemplate GET_HEURISTIC_RESOURCE_NAME_TEMPLATE = + // PathTemplate.create("projects/{project}/locations/{location}/heuristics/{heuristic}"); + TypeNode pathTemplateType = + TypeNode.withReference(ConcreteReference.withClazz(PathTemplate.class)); + String templateName = + String.format("%s_RESOURCE_NAME_TEMPLATE", JavaStyle.toUpperSnakeCase(method.name())); + Variable pathTemplateVar = + Variable.builder().setType(pathTemplateType).setName(templateName).build(); + Statement pathTemplateClassStatement = + createPathTemplateClassStatement(canonicalPath, pathTemplateType, pathTemplateVar); + + classStatements.add(pathTemplateClassStatement); + VariableExpr requestVarExpr = createRequestVarExpr(method); List bodyStatements = new ArrayList<>(); - Expr returnExpr = + + TypeNode mapType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(Map.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + TypeNode hashMapType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(java.util.HashMap.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + + // Expected expression: Map resourceNameSegments = new HashMap(); + VariableExpr resourceNameSegmentsVarExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("resourceNameSegments").setType(mapType).build()) + .setIsDecl(true) + .build(); + + bodyStatements.add( + ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr(resourceNameSegmentsVarExpr) + .setValueExpr( + NewObjectExpr.builder().setType(hashMapType).setIsGeneric(true).build()) + .build())); + + VariableExpr resourceNameSegmentsExpr = + VariableExpr.builder() + .setVariable( + Variable.builder().setName("resourceNameSegments").setType(mapType).build()) + .build(); + + Set httpBindings = method.httpBindings().pathParameters(); + + // For each httpBinding, + // generates resourceNameSegments.put("field",String.valueOf(request.getField())); + for (HttpBindings.HttpBinding httpBinding : httpBindings) { + if (!Pattern.compile("\\{" + httpBinding.name() + "(?:=.*?)?\\}") + .matcher(canonicalPath) + .find()) { + continue; + } + MethodInvocationExpr getFieldExpr = + createRequestFieldGetterExpr( + requestVarExpr, + httpBinding.name(), + httpBinding.field() != null && httpBinding.field().isEnum()); + + MethodInvocationExpr putExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(resourceNameSegmentsExpr) + .setMethodName("put") + .setArguments( + ValueExpr.withValue(StringObjectValue.withValue(httpBinding.name())), + MethodInvocationExpr.builder() + .setStaticReferenceType(TypeNode.STRING) + .setMethodName("valueOf") + .setArguments(getFieldExpr) + .setReturnType(TypeNode.STRING) + .build()) + .build(); + bodyStatements.add(ExprStatement.withExpr(putExpr)); + } + + MethodInvocationExpr instantiateExpr = MethodInvocationExpr.builder() - .setExprReferenceExpr(requestVarExpr) - .setMethodName( - String.format("get%s", JavaStyle.toUpperCamelCase(resourceNameField.name()))) + .setExprReferenceExpr(VariableExpr.builder().setVariable(pathTemplateVar).build()) + .setMethodName("instantiate") + .setArguments(resourceNameSegmentsExpr) .setReturnType(TypeNode.STRING) .build(); return LambdaExpr.builder() .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) .setBody(bodyStatements) - .setReturnExpr(returnExpr) + .setReturnExpr(instantiateExpr) .build(); } @@ -1698,7 +1837,8 @@ private void createRequestParamsExtractorBodyForRoutingHeaders( Variable.builder().setType(pathTemplateType).setName(pathTemplateName).build(); Expr routingHeaderPatternExpr = VariableExpr.withVariable(pathTemplateVar); Statement pathTemplateClassVar = - createPathTemplateClassStatement(routingHeaderParam, pathTemplateType, pathTemplateVar); + createPathTemplateClassStatement( + routingHeaderParam.pattern(), pathTemplateType, pathTemplateVar); classStatements.add(pathTemplateClassVar); MethodInvocationExpr addParamMethodExpr = MethodInvocationExpr.builder() @@ -1728,9 +1868,7 @@ private void createRequestParamsExtractorBodyForRoutingHeaders( } private Statement createPathTemplateClassStatement( - RoutingHeaderRule.RoutingHeaderParam routingHeaderParam, - TypeNode pathTemplateType, - Variable pathTemplateVar) { + String pattern, TypeNode pathTemplateType, Variable pathTemplateVar) { VariableExpr pathTemplateVarExpr = VariableExpr.builder() .setVariable(pathTemplateVar) @@ -1739,8 +1877,7 @@ private Statement createPathTemplateClassStatement( .setIsFinal(true) .setScope(ScopeNode.PRIVATE) .build(); - ValueExpr valueExpr = - ValueExpr.withValue(StringObjectValue.withValue(routingHeaderParam.pattern())); + ValueExpr valueExpr = ValueExpr.withValue(StringObjectValue.withValue(pattern)); Expr pathTemplateExpr = AssignmentExpr.builder() .setVariableExpr(pathTemplateVarExpr) @@ -1825,4 +1962,52 @@ private static VariableExpr createRequestVarExpr(Method method) { return VariableExpr.withVariable( Variable.builder().setType(method.inputType()).setName("request").build()); } + + private static Set getKnownResources(Service service) { + + Set knownResources = new HashSet<>(); + for (Method method : service.methods()) { + if (!method.hasHttpBindings()) { + continue; + } + if (isAipStandardMethod(AIP_STANDARDS_METHODS, method.name())) { + for (String pattern : method.httpBindings().additionalPatterns()) { + knownResources.addAll(extractLiteralSegments(pattern)); + } + knownResources.addAll(extractLiteralSegments(method.httpBindings().pattern())); + } + } + return knownResources; + } + + private static boolean isAipStandardMethod(List standards, String methodName) { + return standards.stream().anyMatch(standard -> standard.equalsIgnoreCase(methodName)); + } + + private static Set extractLiteralSegments(String pattern) { + if (pattern == null || pattern.isEmpty()) { + return new HashSet<>(); + } + try { + PathTemplate template = PathTemplate.create(pattern); + return template.getResourceLiterals(); + } catch (Exception e) { + return new HashSet<>(); + } + } + + private static String extractCanonicalResourceName(String pattern, Set knownResources) { + if (pattern == null + || pattern.isEmpty() + || knownResources == null + || knownResources.isEmpty()) { + return ""; + } + try { + PathTemplate template = PathTemplate.create(pattern); + return template.getCanonicalResourceName(knownResources); + } catch (Exception e) { + return ""; + } + } } diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResourceNameExtractorStub.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResourceNameExtractorStub.golden index 371e29f394..4657fbe7da 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResourceNameExtractorStub.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcResourceNameExtractorStub.golden @@ -1,4 +1,4 @@ -package com.google.extractor.testing.stub; +package com.google.cloud.bigquery.testing.stub; import com.google.api.gax.core.BackgroundResource; import com.google.api.gax.core.BackgroundResourceAggregation; @@ -7,16 +7,25 @@ import com.google.api.gax.grpc.GrpcStubCallableFactory; import com.google.api.gax.rpc.ClientContext; import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.UnaryCallable; -import com.google.extractor.testing.Bar; -import com.google.extractor.testing.Foo; -import com.google.extractor.testing.GetBarRequest; -import com.google.extractor.testing.GetFooRequest; -import com.google.extractor.testing.ListFoosRequest; -import com.google.extractor.testing.ListFoosResponse; +import com.google.api.pathtemplate.PathTemplate; +import com.google.cloud.bigquery.testing.Bar; +import com.google.cloud.bigquery.testing.Foo; +import com.google.cloud.bigquery.testing.GetBarRequest; +import com.google.cloud.bigquery.testing.GetFooRequest; +import com.google.cloud.bigquery.testing.GetHeuristicRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithNamedBindingRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithNestedFieldsRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithResourceReferenceRequest; +import com.google.cloud.bigquery.testing.Heuristic; +import com.google.cloud.bigquery.testing.ListFoosRequest; +import com.google.cloud.bigquery.testing.ListFoosResponse; +import com.google.cloud.bigquery.testing.ListHeuristicsRequest; +import com.google.cloud.bigquery.testing.ListHeuristicsResponse; import com.google.longrunning.stub.GrpcOperationsStub; import io.grpc.MethodDescriptor; import io.grpc.protobuf.ProtoUtils; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Generated; @@ -32,7 +41,7 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT private static final MethodDescriptor getFooMethodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/GetFoo") + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetFoo") .setRequestMarshaller(ProtoUtils.marshaller(GetFooRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(Foo.getDefaultInstance())) .setSampledToLocalTracing(true) @@ -41,30 +50,126 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT private static final MethodDescriptor getBarMethodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/GetBar") + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetBar") .setRequestMarshaller(ProtoUtils.marshaller(GetBarRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(Bar.getDefaultInstance())) .setSampledToLocalTracing(true) .build(); + private static final MethodDescriptor + getHeuristicMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristic") + .setRequestMarshaller(ProtoUtils.marshaller(GetHeuristicRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Heuristic.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + private static final MethodDescriptor listFoosMethodDescriptor = MethodDescriptor.newBuilder() .setType(MethodDescriptor.MethodType.UNARY) - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/ListFoos") + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/ListFoos") .setRequestMarshaller(ProtoUtils.marshaller(ListFoosRequest.getDefaultInstance())) .setResponseMarshaller(ProtoUtils.marshaller(ListFoosResponse.getDefaultInstance())) .setSampledToLocalTracing(true) .build(); + private static final MethodDescriptor + getMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/Get") + .setRequestMarshaller( + ProtoUtils.marshaller(GetHeuristicWithNamedBindingRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Heuristic.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + + private static final MethodDescriptor + listMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/List") + .setRequestMarshaller( + ProtoUtils.marshaller(ListHeuristicsRequest.getDefaultInstance())) + .setResponseMarshaller( + ProtoUtils.marshaller(ListHeuristicsResponse.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + + private static final MethodDescriptor + getHeuristicWithResourceReferenceMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithResourceReference") + .setRequestMarshaller( + ProtoUtils.marshaller( + GetHeuristicWithResourceReferenceRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Heuristic.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + + private static final MethodDescriptor + getHeuristicWithNestedFieldsMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithNestedFields") + .setRequestMarshaller( + ProtoUtils.marshaller(GetHeuristicWithNestedFieldsRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Heuristic.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + + private static final MethodDescriptor + getHeuristicWithNamedBindingMethodDescriptor = + MethodDescriptor.newBuilder() + .setType(MethodDescriptor.MethodType.UNARY) + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithNamedBinding") + .setRequestMarshaller( + ProtoUtils.marshaller(GetHeuristicWithNamedBindingRequest.getDefaultInstance())) + .setResponseMarshaller(ProtoUtils.marshaller(Heuristic.getDefaultInstance())) + .setSampledToLocalTracing(true) + .build(); + private final UnaryCallable getFooCallable; private final UnaryCallable getBarCallable; + private final UnaryCallable getHeuristicCallable; private final UnaryCallable listFoosCallable; + private final UnaryCallable getCallable; + private final UnaryCallable listCallable; + private final UnaryCallable + getHeuristicWithResourceReferenceCallable; + private final UnaryCallable + getHeuristicWithNestedFieldsCallable; + private final UnaryCallable + getHeuristicWithNamedBindingCallable; private final BackgroundResource backgroundResources; private final GrpcOperationsStub operationsStub; private final GrpcStubCallableFactory callableFactory; + private static final PathTemplate GET_BAR_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("{bar=projects/*/locations/*/bars/*}"); + private static final PathTemplate GET_HEURISTIC_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}/heuristics/{heuristic}"); + private static final PathTemplate GET_RESOURCE_NAME_TEMPLATE = + PathTemplate.create( + "projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}"); + private static final PathTemplate LIST_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}"); + private static final PathTemplate GET_HEURISTIC_WITH_NESTED_FIELDS_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}/heuristics/{heuristic.name}"); + private static final PathTemplate GET_HEURISTIC_WITH_NAMED_BINDING_RESOURCE_NAME_TEMPLATE = + PathTemplate.create( + "projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}"); + public static final GrpcResourceNameExtractorTestingStub create( ResourceNameExtractorTestingStubSettings settings) throws IOException { return new GrpcResourceNameExtractorTestingStub(settings, ClientContext.create(settings)); @@ -128,6 +233,32 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT builder.add("bar", String.valueOf(request.getBar())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("bar", String.valueOf(request.getBar())); + return GET_BAR_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + GrpcCallSettings getHeuristicTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); GrpcCallSettings listFoosTransportSettings = GrpcCallSettings.newBuilder() @@ -140,6 +271,109 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT }) .setResourceNameExtractor(request -> request.getParent()) .build(); + GrpcCallSettings getTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("parent_name", String.valueOf(request.getParentName())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("parent_name", String.valueOf(request.getParentName())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + GrpcCallSettings listTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(listMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return LIST_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + GrpcCallSettings + getHeuristicWithResourceReferenceTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithResourceReferenceMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor(request -> request.getFoo()) + .build(); + GrpcCallSettings + getHeuristicWithNestedFieldsTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithNestedFieldsMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + "heuristic.name", String.valueOf(request.getHeuristic().getName())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put( + "heuristic.name", String.valueOf(request.getHeuristic().getName())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_WITH_NESTED_FIELDS_RESOURCE_NAME_TEMPLATE.instantiate( + resourceNameSegments); + }) + .build(); + GrpcCallSettings + getHeuristicWithNamedBindingTransportSettings = + GrpcCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithNamedBindingMethodDescriptor) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("parent_name", String.valueOf(request.getParentName())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put( + "parent_name", String.valueOf(request.getParentName())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_WITH_NAMED_BINDING_RESOURCE_NAME_TEMPLATE.instantiate( + resourceNameSegments); + }) + .build(); this.getFooCallable = callableFactory.createUnaryCallable( @@ -147,9 +381,33 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT this.getBarCallable = callableFactory.createUnaryCallable( getBarTransportSettings, settings.getBarSettings(), clientContext); + this.getHeuristicCallable = + callableFactory.createUnaryCallable( + getHeuristicTransportSettings, settings.getHeuristicSettings(), clientContext); this.listFoosCallable = callableFactory.createUnaryCallable( listFoosTransportSettings, settings.listFoosSettings(), clientContext); + this.getCallable = + callableFactory.createUnaryCallable( + getTransportSettings, settings.getSettings(), clientContext); + this.listCallable = + callableFactory.createUnaryCallable( + listTransportSettings, settings.listSettings(), clientContext); + this.getHeuristicWithResourceReferenceCallable = + callableFactory.createUnaryCallable( + getHeuristicWithResourceReferenceTransportSettings, + settings.getHeuristicWithResourceReferenceSettings(), + clientContext); + this.getHeuristicWithNestedFieldsCallable = + callableFactory.createUnaryCallable( + getHeuristicWithNestedFieldsTransportSettings, + settings.getHeuristicWithNestedFieldsSettings(), + clientContext); + this.getHeuristicWithNamedBindingCallable = + callableFactory.createUnaryCallable( + getHeuristicWithNamedBindingTransportSettings, + settings.getHeuristicWithNamedBindingSettings(), + clientContext); this.backgroundResources = new BackgroundResourceAggregation(clientContext.getBackgroundResources()); @@ -169,11 +427,44 @@ public class GrpcResourceNameExtractorTestingStub extends ResourceNameExtractorT return getBarCallable; } + @Override + public UnaryCallable getHeuristicCallable() { + return getHeuristicCallable; + } + @Override public UnaryCallable listFoosCallable() { return listFoosCallable; } + @Override + public UnaryCallable getCallable() { + return getCallable; + } + + @Override + public UnaryCallable listCallable() { + return listCallable; + } + + @Override + public UnaryCallable + getHeuristicWithResourceReferenceCallable() { + return getHeuristicWithResourceReferenceCallable; + } + + @Override + public UnaryCallable + getHeuristicWithNestedFieldsCallable() { + return getHeuristicWithNestedFieldsCallable; + } + + @Override + public UnaryCallable + getHeuristicWithNamedBindingCallable() { + return getHeuristicWithNamedBindingCallable; + } + @Override public final void close() { try { diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResourceNameExtractorStub.golden b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResourceNameExtractorStub.golden index c3296eb841..ee8722706b 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResourceNameExtractorStub.golden +++ b/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/goldens/HttpJsonResourceNameExtractorStub.golden @@ -1,4 +1,4 @@ -package com.google.extractor.testing.stub; +package com.google.cloud.bigquery.testing.stub; import com.google.api.core.InternalApi; import com.google.api.gax.core.BackgroundResource; @@ -12,12 +12,20 @@ import com.google.api.gax.httpjson.ProtoRestSerializer; import com.google.api.gax.rpc.ClientContext; import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.UnaryCallable; -import com.google.extractor.testing.Bar; -import com.google.extractor.testing.Foo; -import com.google.extractor.testing.GetBarRequest; -import com.google.extractor.testing.GetFooRequest; -import com.google.extractor.testing.ListFoosRequest; -import com.google.extractor.testing.ListFoosResponse; +import com.google.api.pathtemplate.PathTemplate; +import com.google.cloud.bigquery.testing.Bar; +import com.google.cloud.bigquery.testing.Foo; +import com.google.cloud.bigquery.testing.GetBarRequest; +import com.google.cloud.bigquery.testing.GetFooRequest; +import com.google.cloud.bigquery.testing.GetHeuristicRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithNamedBindingRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithNestedFieldsRequest; +import com.google.cloud.bigquery.testing.GetHeuristicWithResourceReferenceRequest; +import com.google.cloud.bigquery.testing.Heuristic; +import com.google.cloud.bigquery.testing.ListFoosRequest; +import com.google.cloud.bigquery.testing.ListFoosResponse; +import com.google.cloud.bigquery.testing.ListHeuristicsRequest; +import com.google.cloud.bigquery.testing.ListHeuristicsResponse; import com.google.protobuf.TypeRegistry; import java.io.IOException; import java.util.ArrayList; @@ -39,7 +47,7 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac private static final ApiMethodDescriptor getFooMethodDescriptor = ApiMethodDescriptor.newBuilder() - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/GetFoo") + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetFoo") .setHttpMethod("GET") .setType(ApiMethodDescriptor.MethodType.UNARY) .setRequestFormatter( @@ -71,7 +79,7 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac private static final ApiMethodDescriptor getBarMethodDescriptor = ApiMethodDescriptor.newBuilder() - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/GetBar") + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetBar") .setHttpMethod("GET") .setType(ApiMethodDescriptor.MethodType.UNARY) .setRequestFormatter( @@ -101,10 +109,47 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac .build()) .build(); + private static final ApiMethodDescriptor + getHeuristicMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristic") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/heuristics/{heuristic}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putPathParam(fields, "heuristic", request.getHeuristic()); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Heuristic.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + private static final ApiMethodDescriptor listFoosMethodDescriptor = ApiMethodDescriptor.newBuilder() - .setFullMethodName("google.extractor.testing.ResourceNameExtractorTesting/ListFoos") + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/ListFoos") .setHttpMethod("GET") .setType(ApiMethodDescriptor.MethodType.UNARY) .setRequestFormatter( @@ -134,13 +179,220 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac .build()) .build(); + private static final ApiMethodDescriptor + getMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/Get") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/{parentName=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putPathParam(fields, "heuristic", request.getHeuristic()); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "parentName", request.getParentName()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Heuristic.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + + private static final ApiMethodDescriptor + listMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.bigquery.testing.ResourceNameExtractorTesting/List") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/heuristics", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(ListHeuristicsResponse.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + + private static final ApiMethodDescriptor + getHeuristicWithResourceReferenceMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithResourceReference") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter + .newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/heuristics/{heuristic}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer + serializer = ProtoRestSerializer.create(); + serializer.putPathParam(fields, "heuristic", request.getHeuristic()); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer + serializer = ProtoRestSerializer.create(); + serializer.putQueryParam(fields, "foo", request.getFoo()); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Heuristic.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + + private static final ApiMethodDescriptor + getHeuristicWithNestedFieldsMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithNestedFields") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/heuristics/{heuristic.name}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putPathParam( + fields, "heuristic.name", request.getHeuristic().getName()); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putQueryParam(fields, "heuristic", request.getHeuristic()); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Heuristic.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + + private static final ApiMethodDescriptor + getHeuristicWithNamedBindingMethodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName( + "google.cloud.bigquery.testing.ResourceNameExtractorTesting/GetHeuristicWithNamedBinding") + .setHttpMethod("GET") + .setType(ApiMethodDescriptor.MethodType.UNARY) + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/v1/projects/{project}/locations/{location}/{parentName=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + serializer.putPathParam(fields, "heuristic", request.getHeuristic()); + serializer.putPathParam(fields, "location", request.getLocation()); + serializer.putPathParam(fields, "parentName", request.getParentName()); + serializer.putPathParam(fields, "project", request.getProject()); + return fields; + }) + .setQueryParamsExtractor( + request -> { + Map> fields = new HashMap<>(); + ProtoRestSerializer serializer = + ProtoRestSerializer.create(); + return fields; + }) + .setRequestBodyExtractor(request -> null) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Heuristic.getDefaultInstance()) + .setDefaultTypeRegistry(typeRegistry) + .build()) + .build(); + private final UnaryCallable getFooCallable; private final UnaryCallable getBarCallable; + private final UnaryCallable getHeuristicCallable; private final UnaryCallable listFoosCallable; + private final UnaryCallable getCallable; + private final UnaryCallable listCallable; + private final UnaryCallable + getHeuristicWithResourceReferenceCallable; + private final UnaryCallable + getHeuristicWithNestedFieldsCallable; + private final UnaryCallable + getHeuristicWithNamedBindingCallable; private final BackgroundResource backgroundResources; private final HttpJsonStubCallableFactory callableFactory; + private static final PathTemplate GET_BAR_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("{bar=projects/*/locations/*/bars/*}"); + private static final PathTemplate GET_HEURISTIC_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}/heuristics/{heuristic}"); + private static final PathTemplate GET_RESOURCE_NAME_TEMPLATE = + PathTemplate.create( + "projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}"); + private static final PathTemplate LIST_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}"); + private static final PathTemplate GET_HEURISTIC_WITH_NESTED_FIELDS_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/locations/{location}/heuristics/{heuristic.name}"); + private static final PathTemplate GET_HEURISTIC_WITH_NAMED_BINDING_RESOURCE_NAME_TEMPLATE = + PathTemplate.create( + "projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}"); + public static final HttpJsonResourceNameExtractorTestingStub create( ResourceNameExtractorTestingStubSettings settings) throws IOException { return new HttpJsonResourceNameExtractorTestingStub(settings, ClientContext.create(settings)); @@ -205,6 +457,33 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac builder.add("bar", String.valueOf(request.getBar())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("bar", String.valueOf(request.getBar())); + return GET_BAR_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + HttpJsonCallSettings getHeuristicTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); HttpJsonCallSettings listFoosTransportSettings = HttpJsonCallSettings.newBuilder() @@ -218,6 +497,114 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac }) .setResourceNameExtractor(request -> request.getParent()) .build(); + HttpJsonCallSettings getTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(getMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("parent_name", String.valueOf(request.getParentName())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("parent_name", String.valueOf(request.getParentName())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + HttpJsonCallSettings listTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(listMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return LIST_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) + .build(); + HttpJsonCallSettings + getHeuristicWithResourceReferenceTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithResourceReferenceMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor(request -> request.getFoo()) + .build(); + HttpJsonCallSettings + getHeuristicWithNestedFieldsTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithNestedFieldsMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add( + "heuristic.name", String.valueOf(request.getHeuristic().getName())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put( + "heuristic.name", String.valueOf(request.getHeuristic().getName())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_WITH_NESTED_FIELDS_RESOURCE_NAME_TEMPLATE.instantiate( + resourceNameSegments); + }) + .build(); + HttpJsonCallSettings + getHeuristicWithNamedBindingTransportSettings = + HttpJsonCallSettings.newBuilder() + .setMethodDescriptor(getHeuristicWithNamedBindingMethodDescriptor) + .setTypeRegistry(typeRegistry) + .setParamsExtractor( + request -> { + RequestParamsBuilder builder = RequestParamsBuilder.create(); + builder.add("heuristic", String.valueOf(request.getHeuristic())); + builder.add("location", String.valueOf(request.getLocation())); + builder.add("parent_name", String.valueOf(request.getParentName())); + builder.add("project", String.valueOf(request.getProject())); + return builder.build(); + }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("heuristic", String.valueOf(request.getHeuristic())); + resourceNameSegments.put("location", String.valueOf(request.getLocation())); + resourceNameSegments.put( + "parent_name", String.valueOf(request.getParentName())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return GET_HEURISTIC_WITH_NAMED_BINDING_RESOURCE_NAME_TEMPLATE.instantiate( + resourceNameSegments); + }) + .build(); this.getFooCallable = callableFactory.createUnaryCallable( @@ -225,9 +612,33 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac this.getBarCallable = callableFactory.createUnaryCallable( getBarTransportSettings, settings.getBarSettings(), clientContext); + this.getHeuristicCallable = + callableFactory.createUnaryCallable( + getHeuristicTransportSettings, settings.getHeuristicSettings(), clientContext); this.listFoosCallable = callableFactory.createUnaryCallable( listFoosTransportSettings, settings.listFoosSettings(), clientContext); + this.getCallable = + callableFactory.createUnaryCallable( + getTransportSettings, settings.getSettings(), clientContext); + this.listCallable = + callableFactory.createUnaryCallable( + listTransportSettings, settings.listSettings(), clientContext); + this.getHeuristicWithResourceReferenceCallable = + callableFactory.createUnaryCallable( + getHeuristicWithResourceReferenceTransportSettings, + settings.getHeuristicWithResourceReferenceSettings(), + clientContext); + this.getHeuristicWithNestedFieldsCallable = + callableFactory.createUnaryCallable( + getHeuristicWithNestedFieldsTransportSettings, + settings.getHeuristicWithNestedFieldsSettings(), + clientContext); + this.getHeuristicWithNamedBindingCallable = + callableFactory.createUnaryCallable( + getHeuristicWithNamedBindingTransportSettings, + settings.getHeuristicWithNamedBindingSettings(), + clientContext); this.backgroundResources = new BackgroundResourceAggregation(clientContext.getBackgroundResources()); @@ -238,7 +649,13 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac List methodDescriptors = new ArrayList<>(); methodDescriptors.add(getFooMethodDescriptor); methodDescriptors.add(getBarMethodDescriptor); + methodDescriptors.add(getHeuristicMethodDescriptor); methodDescriptors.add(listFoosMethodDescriptor); + methodDescriptors.add(getMethodDescriptor); + methodDescriptors.add(listMethodDescriptor); + methodDescriptors.add(getHeuristicWithResourceReferenceMethodDescriptor); + methodDescriptors.add(getHeuristicWithNestedFieldsMethodDescriptor); + methodDescriptors.add(getHeuristicWithNamedBindingMethodDescriptor); return methodDescriptors; } @@ -252,11 +669,44 @@ public class HttpJsonResourceNameExtractorTestingStub extends ResourceNameExtrac return getBarCallable; } + @Override + public UnaryCallable getHeuristicCallable() { + return getHeuristicCallable; + } + @Override public UnaryCallable listFoosCallable() { return listFoosCallable; } + @Override + public UnaryCallable getCallable() { + return getCallable; + } + + @Override + public UnaryCallable listCallable() { + return listCallable; + } + + @Override + public UnaryCallable + getHeuristicWithResourceReferenceCallable() { + return getHeuristicWithResourceReferenceCallable; + } + + @Override + public UnaryCallable + getHeuristicWithNestedFieldsCallable() { + return getHeuristicWithNestedFieldsCallable; + } + + @Override + public UnaryCallable + getHeuristicWithNamedBindingCallable() { + return getHeuristicWithNamedBindingCallable; + } + @Override public final void close() { try { diff --git a/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java b/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java index a4c4a3afb5..b14d36e828 100644 --- a/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java +++ b/gapic-generator-java/src/test/java/com/google/api/generator/test/protoloader/TestProtoLoader.java @@ -31,9 +31,9 @@ import com.google.api.version.test.ApiVersionTestingOuterClass; import com.google.auto.populate.field.AutoPopulateFieldTestingOuterClass; import com.google.bookshop.v1beta1.BookshopProto; +import com.google.cloud.bigquery.testing.ResourceNameExtractorTestingOuterClass; import com.google.cloud.bigquery.v2.JobProto; import com.google.explicit.dynamic.routing.header.ExplicitDynamicRoutingHeaderTestingOuterClass; -import com.google.extractor.testing.ResourceNameExtractorTestingOuterClass; import com.google.logging.v2.LogEntryProto; import com.google.logging.v2.LoggingConfigProto; import com.google.logging.v2.LoggingMetricsProto; diff --git a/gapic-generator-java/src/test/proto/resource_name_extractor_testing.proto b/gapic-generator-java/src/test/proto/resource_name_extractor_testing.proto index f339db4b93..1dd59b24cf 100644 --- a/gapic-generator-java/src/test/proto/resource_name_extractor_testing.proto +++ b/gapic-generator-java/src/test/proto/resource_name_extractor_testing.proto @@ -1,10 +1,10 @@ syntax = "proto3"; -package google.extractor.testing; +package google.cloud.bigquery.testing; option java_multiple_files = true; option java_outer_classname = "ResourceNameExtractorTestingOuterClass"; -option java_package = "com.google.extractor.testing"; +option java_package = "com.google.cloud.bigquery.testing"; import "google/api/annotations.proto"; import "google/api/client.proto"; @@ -27,14 +27,73 @@ service ResourceNameExtractorTesting { option (google.api.method_signature) = "bar"; } + rpc GetHeuristic(GetHeuristicRequest) returns (Heuristic) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/heuristics/{heuristic}" + }; + } + rpc ListFoos(ListFoosRequest) returns (ListFoosResponse) { option (google.api.http) = { get: "/v1/{parent=projects/*/locations/*}/foos" }; option (google.api.method_signature) = "parent"; } + + // Exact AIP Method for populating known resources + rpc Get(GetHeuristicWithNamedBindingRequest) returns (Heuristic) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}" + }; + } + + rpc List(ListHeuristicsRequest) returns (ListHeuristicsResponse) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/heuristics" + }; + } + + // Should NOT generate a heuristic, because it has a resource_reference field + rpc GetHeuristicWithResourceReference(GetHeuristicWithResourceReferenceRequest) returns (Heuristic) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/heuristics/{heuristic}" + }; + } + + + rpc GetHeuristicWithNestedFields(GetHeuristicWithNestedFieldsRequest) returns (Heuristic) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/heuristics/{heuristic.name}" + }; + } + + rpc GetHeuristicWithNamedBinding(GetHeuristicWithNamedBindingRequest) returns (Heuristic) { + option (google.api.http) = { + get: "/v1/projects/{project}/locations/{location}/{parent_name=reservations/*/reservationBlocks/*/reservationSubBlocks/*}/heuristics/{heuristic}" + }; + } +} + +message ListHeuristicsRequest { + string project = 1; + string location = 2; +} +message ListHeuristicsResponse { + repeated Heuristic heuristics = 1; +} + +message GetHeuristicWithResourceReferenceRequest { + string project = 1; + string location = 2; + string heuristic = 3; + string foo = 4 [ + (google.api.resource_reference) = { + type: "extractor.googleapis.com/Foo" + } + ]; } + message Foo { option (google.api.resource) = { type: "extractor.googleapis.com/Foo" @@ -47,6 +106,29 @@ message Bar { string name = 1; } +message GetHeuristicRequest { + string project = 1; + string location = 2; + string heuristic = 3; +} + +message GetHeuristicWithNestedFieldsRequest { + string project = 1; + string location = 2; + Heuristic heuristic = 3; +} + +message GetHeuristicWithNamedBindingRequest { + string project = 1; + string location = 2; + string parent_name = 3; + string heuristic = 4; +} + +message Heuristic { + string name = 1; +} + message GetFooRequest { string name = 1 [ (google.api.resource_reference) = { diff --git a/gax-java/README.md b/gax-java/README.md index afcd206cc1..f6e6935aa4 100644 --- a/gax-java/README.md +++ b/gax-java/README.md @@ -182,5 +182,4 @@ License BSD - See [LICENSE] for more information. [CONTRIBUTING]:https://github.com/googleapis/gax-java/blob/main/CONTRIBUTING.md -[LICENSE]: https://github.com/googleapis/gax-java/blob/main/LICENSE - +[LICENSE]: https://github.com/googleapis/gax-java/blob/main/LICENSE \ No newline at end of file diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties index 94482c46e7..47b1e3f6c3 100644 --- a/gax-java/dependencies.properties +++ b/gax-java/dependencies.properties @@ -95,4 +95,4 @@ maven.io_opentelemetry_opentelemetry_sdk_testing=io.opentelemetry:opentelemetry- maven.io_opentelemetry_opentelemetry_sdk=io.opentelemetry:opentelemetry-sdk:1.51.0 maven.io_opentelemetry_opentelemetry_sdk_common=io.opentelemetry:opentelemetry-sdk-common:1.51.0 maven.io_opentelemetry_opentelemetry_sdk_metrics=io.opentelemetry:opentelemetry-sdk-metrics:1.51.0 -maven.com_google_guava_guava_testlib=com.google.guava:guava-testlib:32.1.3-jre +maven.com_google_guava_guava_testlib=com.google.guava:guava-testlib:32.1.3-jre \ No newline at end of file diff --git a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java index e797842c9e..47e62ae9f6 100644 --- a/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java +++ b/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcClientCalls.java @@ -90,9 +90,19 @@ public static ClientCall newCall( channel = ((ChannelPool) channel).getChannel(grpcContext.getChannelAffinity()); } - if (!grpcContext.getExtraHeaders().isEmpty()) { - ClientInterceptor interceptor = - MetadataUtils.newAttachHeadersInterceptor(grpcContext.getMetadata()); + java.util.Map traceContext = new java.util.HashMap<>(); + grpcContext.getTracer().injectTraceContext(traceContext); + + if (!grpcContext.getExtraHeaders().isEmpty() || !traceContext.isEmpty()) { + Metadata metadata = new Metadata(); + metadata.merge(grpcContext.getMetadata()); + for (java.util.Map.Entry entry : traceContext.entrySet()) { + Metadata.Key key = + Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER); + metadata.removeAll(key); + metadata.put(key, entry.getValue()); + } + ClientInterceptor interceptor = MetadataUtils.newAttachHeadersInterceptor(metadata); channel = ClientInterceptors.intercept(channel, interceptor); } diff --git a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcClientCallsTest.java b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcClientCallsTest.java index 37c29d63da..0e21fac243 100644 --- a/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcClientCallsTest.java +++ b/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcClientCallsTest.java @@ -340,4 +340,48 @@ void testUniverseDomainNotReady_shouldRetry() throws IOException { Truth.assertThat(exception.isRetryable()).isTrue(); Mockito.verify(mockChannel, Mockito.never()).newCall(descriptor, callOptions); } + + @Test + void testTraceContextHeaders() throws IOException { + Metadata emptyHeaders = new Metadata(); + + MethodDescriptor descriptor = FakeServiceGrpc.METHOD_RECOGNIZE; + + @SuppressWarnings("unchecked") + ClientCall mockClientCall = Mockito.mock(ClientCall.class); + + @SuppressWarnings("unchecked") + ClientCall.Listener mockListener = Mockito.mock(ClientCall.Listener.class); + + Channel mockChannel = Mockito.mock(ManagedChannel.class); + com.google.api.gax.tracing.ApiTracer mockTracer = + Mockito.mock(com.google.api.gax.tracing.ApiTracer.class); + + Mockito.doAnswer( + invocation -> { + java.util.Map carrier = invocation.getArgument(0); + carrier.put("traceparent", "00-00000000000000000000000000000001-0000000000000002-01"); + return null; + }) + .when(mockTracer) + .injectTraceContext(Mockito.anyMap()); + + Mockito.doAnswer( + invocation -> { + Metadata clientCallHeaders = (Metadata) invocation.getArguments()[1]; + Metadata.Key traceparentKey = + Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER); + assertThat(clientCallHeaders.getAll(traceparentKey)) + .containsExactly("00-00000000000000000000000000000001-0000000000000002-01"); + return null; + }) + .when(mockClientCall) + .start(Mockito.>any(), Mockito.any()); + + Mockito.when(mockChannel.newCall(Mockito.eq(descriptor), Mockito.any())) + .thenReturn(mockClientCall); + + GrpcCallContext context = defaultCallContext.withChannel(mockChannel).withTracer(mockTracer); + GrpcClientCalls.newCall(descriptor, context).start(mockListener, emptyHeaders); + } } diff --git a/gax-java/gax-httpjson/pom.xml b/gax-java/gax-httpjson/pom.xml index 259b360ba4..2120755f58 100644 --- a/gax-java/gax-httpjson/pom.xml +++ b/gax-java/gax-httpjson/pom.xml @@ -3,7 +3,7 @@ 4.0.0 gax-httpjson - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT jar GAX (Google Api eXtensions) for Java (HTTP JSON) Google Api eXtensions for Java (HTTP JSON) diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java index 2b2d675a2a..c946e9aab0 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallContext.java @@ -607,10 +607,11 @@ public ApiTracer getTracer() { @Override public HttpJsonCallContext withTracer(@Nonnull ApiTracer newTracer) { Preconditions.checkNotNull(newTracer); + HttpJsonCallOptions newCallOptions = callOptions.toBuilder().setTracer(newTracer).build(); return new HttpJsonCallContext( this.channel, - this.callOptions, + newCallOptions, this.timeout, this.streamWaitTimeout, this.streamIdleTimeout, diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java index 4c2d8ae55e..1f0022bb43 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonCallOptions.java @@ -35,6 +35,7 @@ import static com.google.api.gax.util.TimeConversionUtils.toThreetenInstant; import com.google.api.core.ObsoleteApi; +import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import com.google.auto.value.AutoValue; import com.google.protobuf.TypeRegistry; @@ -71,6 +72,9 @@ public final org.threeten.bp.Instant getDeadline() { @Nullable public abstract TypeRegistry getTypeRegistry(); + @Nullable + public abstract ApiTracer getTracer(); + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -106,6 +110,11 @@ public HttpJsonCallOptions merge(HttpJsonCallOptions inputOptions) { builder.setTypeRegistry(newTypeRegistry); } + ApiTracer newTracer = inputOptions.getTracer(); + if (newTracer != null) { + builder.setTracer(newTracer); + } + return builder.build(); } @@ -131,6 +140,8 @@ public final Builder setDeadline(org.threeten.bp.Instant value) { public abstract Builder setTypeRegistry(TypeRegistry value); + public abstract Builder setTracer(ApiTracer value); + public abstract HttpJsonCallOptions build(); } } diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java index df9a507519..53a4dd66d3 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCallImpl.java @@ -156,6 +156,11 @@ public void setResult(RunnableResult runnableResult) { if (runnableResult.getResponseHeaders() != null) { pendingNotifications.offer( new OnHeadersNotificationTask<>(listener, runnableResult.getResponseHeaders())); + if (callOptions.getTracer() != null) { + callOptions + .getTracer() + .responseHeadersReceived(runnableResult.getResponseHeaders().getHeaders()); + } } } @@ -428,6 +433,7 @@ private boolean consumeMessageFromStream() throws IOException { ResponseT message = methodDescriptor.getResponseParser().parse(responseReader, callOptions.getTypeRegistry()); + pendingNotifications.offer(new OnMessageNotificationTask<>(listener, message)); return allMessagesConsumed; diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java index c98d00afb9..6a2f974011 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonClientCalls.java @@ -80,15 +80,25 @@ public static HttpJsonClientCall newC return httpJsonContext.getChannel().newCall(methodDescriptor, httpJsonContext.getCallOptions()); } + static HttpJsonMetadata getMetadataWithTraceContext(HttpJsonCallContext context) { + java.util.Map traceHeaders = new java.util.HashMap<>(); + context.getTracer().injectTraceContext(traceHeaders); + + java.util.Map> finalHeaders = + new java.util.HashMap<>(context.getExtraHeaders()); + for (java.util.Map.Entry entry : traceHeaders.entrySet()) { + finalHeaders.put(entry.getKey(), java.util.Collections.singletonList(entry.getValue())); + } + return HttpJsonMetadata.newBuilder().build().withHeaders(finalHeaders); + } + static ApiFuture futureUnaryCall( HttpJsonClientCall clientCall, RequestT request, HttpJsonCallContext context) { // Start the call HttpJsonFuture future = new HttpJsonFuture<>(clientCall); - clientCall.start( - new FutureListener<>(future), - HttpJsonMetadata.newBuilder().build().withHeaders(context.getExtraHeaders())); + clientCall.start(new FutureListener<>(future), getMetadataWithTraceContext(context)); // Send the request try { diff --git a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java index 2738844bd0..b3b5b2612d 100644 --- a/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java +++ b/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpRequestRunnable.java @@ -44,6 +44,7 @@ import com.google.api.client.json.JsonObjectParser; import com.google.api.client.json.gson.GsonFactory; import com.google.api.client.util.GenericData; +import com.google.api.gax.tracing.ApiTracer; import com.google.auth.Credentials; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auto.value.AutoValue; @@ -188,6 +189,11 @@ HttpRequest createHttpRequest() throws IOException { } } + ApiTracer tracer = httpJsonCallOptions.getTracer(); + if (tracer != null) { + tracer.requestUrlResolved(url.build()); + } + HttpRequest httpRequest = buildRequest(requestFactory, url, jsonHttpContent); for (Map.Entry entry : headers.getHeaders().entrySet()) { diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java index 156b4bb039..29bea372e1 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallContextTest.java @@ -252,6 +252,16 @@ void testMergeWithTracer() { .isSameInstanceAs(defaultTracer); } + @Test + void testWithTracer() { + ApiTracer tracer = Mockito.mock(ApiTracer.class); + HttpJsonCallContext emptyContext = HttpJsonCallContext.createDefault(); + // Default context has a default tracer. + assertNotNull(emptyContext.getTracer()); + HttpJsonCallContext context = emptyContext.withTracer(tracer); + Truth.assertThat(context.getTracer()).isSameInstanceAs(tracer); + } + @Test void testWithRetrySettings() { RetrySettings retrySettings = Mockito.mock(RetrySettings.class); diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java index c6aa69d4d8..4a8a2bc6f3 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonCallOptionsTest.java @@ -31,12 +31,22 @@ import static com.google.api.gax.util.TimeConversionTestUtils.testDurationMethod; import static com.google.api.gax.util.TimeConversionTestUtils.testInstantMethod; +import static com.google.common.truth.Truth.assertThat; +import com.google.api.gax.tracing.ApiTracer; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class HttpJsonCallOptionsTest { private final HttpJsonCallOptions.Builder OPTIONS_BUILDER = HttpJsonCallOptions.newBuilder(); + @Test + void testTracer() { + ApiTracer tracer = Mockito.mock(ApiTracer.class); + HttpJsonCallOptions options = OPTIONS_BUILDER.setTracer(tracer).build(); + assertThat(options.getTracer()).isSameInstanceAs(tracer); + } + @Test public void testDeadline() { final long millis = 3; diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java index 2853e79ad5..3d24bd5356 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallImplTest.java @@ -29,15 +29,35 @@ */ package com.google.api.gax.httpjson; +import static com.google.common.truth.Truth.assertThat; + import com.google.api.client.http.HttpTransport; +import com.google.api.gax.httpjson.testing.MockHttpService; +import com.google.api.gax.httpjson.testing.TestApiTracer; +import com.google.api.gax.rpc.EndpointContext; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.StreamController; +import com.google.auth.Credentials; import com.google.common.truth.Truth; +import com.google.protobuf.Field; import com.google.protobuf.TypeRegistry; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.Reader; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -135,4 +155,170 @@ void responseReceived_cancellationTaskExists_isCancelledProperly() throws Interr // Scheduler is not waiting for any task and should terminate quickly Truth.assertThat(deadlineSchedulerExecutor.isTerminated()).isTrue(); } + + private static final ApiMethodDescriptor FAKE_METHOD_DESCRIPTOR = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("google.cloud.v1.Fake/FakeMethod") + .setHttpMethod("POST") + .setRequestFormatter( + ProtoMessageRequestFormatter.newBuilder() + .setPath( + "/fake/v1/name/{name}", + request -> { + Map fields = new HashMap<>(); + ProtoRestSerializer serializer = ProtoRestSerializer.create(); + serializer.putPathParam(fields, "name", request.getName()); + return fields; + }) + .setQueryParamsExtractor(request -> new HashMap<>()) + .setRequestBodyExtractor( + request -> + ProtoRestSerializer.create() + .toBody("*", request.toBuilder().clearName().build(), false)) + .build()) + .setResponseParser( + ProtoMessageResponseParser.newBuilder() + .setDefaultInstance(Field.getDefaultInstance()) + .build()) + .build(); + + private static final MockHttpService MOCK_SERVICE = + new MockHttpService(Collections.singletonList(FAKE_METHOD_DESCRIPTOR), "google.com:443"); + + private static ExecutorService executorService; + private ManagedHttpJsonChannel channel; + private TestApiTracer tracer; + + @BeforeAll + static void initialize() { + executorService = Executors.newFixedThreadPool(2); + } + + @AfterAll + static void destroy() { + executorService.shutdownNow(); + } + + @BeforeEach + void setUp() { + channel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(MOCK_SERVICE) + .build(); + tracer = new TestApiTracer(); + } + + @AfterEach + void tearDown() { + MOCK_SERVICE.reset(); + } + + @Test + void testBodySizeRecording() throws Exception { + HttpJsonDirectCallable callable = + new HttpJsonDirectCallable<>(FAKE_METHOD_DESCRIPTOR); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.lenient() + .doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(channel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response = Field.newBuilder().setName("alice").setNumber(43).build(); + + MOCK_SERVICE.addResponse(response); + + callable.futureCall(request, callContext).get(); + + // Verify response size + // MockHttpService uses ProtoRestSerializer which pretty-prints. + String expectedResponseBody = ProtoRestSerializer.create().toBody("*", response, false); + long expectedResponseSize = expectedResponseBody.getBytes("UTF-8").length; + assertThat(tracer.getResponseReceivedSize()).isEqualTo(expectedResponseSize); + } + + @Test + void testBodySizeRecordingServerStreaming() throws Exception { + ApiMethodDescriptor methodServerStreaming = + FAKE_METHOD_DESCRIPTOR.toBuilder() + .setType(ApiMethodDescriptor.MethodType.SERVER_STREAMING) + .build(); + + MockHttpService streamingMockService = + new MockHttpService(Collections.singletonList(methodServerStreaming), "google.com:443"); + ManagedHttpJsonChannel streamingChannel = + ManagedHttpJsonChannel.newBuilder() + .setEndpoint("google.com:443") + .setExecutor(executorService) + .setHttpTransport(streamingMockService) + .build(); + + HttpJsonDirectServerStreamingCallable callable = + new HttpJsonDirectServerStreamingCallable<>(methodServerStreaming); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + Mockito.lenient() + .doNothing() + .when(endpointContext) + .validateUniverseDomain( + Mockito.any(Credentials.class), Mockito.any(HttpJsonStatusCode.class)); + + HttpJsonCallContext callContext = + HttpJsonCallContext.createDefault() + .withChannel(streamingChannel) + .withEndpointContext(endpointContext) + .withTracer(tracer); + + Field request = Field.newBuilder().setName("bob").setNumber(42).build(); + Field response1 = Field.newBuilder().setName("alice1").setNumber(43).build(); + Field response2 = Field.newBuilder().setName("alice2").setNumber(44).build(); + + streamingMockService.addResponse(new Field[] {response1, response2}); + + final List receivedResponses = new java.util.ArrayList<>(); + final CountDownLatch latch = new CountDownLatch(1); + + callable.call( + request, + new ResponseObserver() { + @Override + public void onStart(StreamController controller) { + // no behavior needed + } + + @Override + public void onResponse(Field response) { + receivedResponses.add(response); + } + + @Override + public void onError(Throwable t) { + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } + }, + callContext); + + latch.await(10, TimeUnit.SECONDS); + + assertThat(receivedResponses).hasSize(2); + + // Verify response size (0 because streaming chunked responses don't include Content-Length) + assertThat(tracer.getResponseReceivedSize()).isEqualTo(0); + streamingChannel.shutdownNow(); + } } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallsTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallsTest.java index 7e2d9cb946..4a8f60d0bc 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallsTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonClientCallsTest.java @@ -139,4 +139,35 @@ void testUniverseDomainNotReady_shouldRetry() throws IOException { .isEqualTo(HttpJsonStatusCode.Code.UNAUTHENTICATED); Mockito.verify(mockChannel, Mockito.never()).newCall(descriptor, callOptions); } + + @Test + void testGetMetadataWithTraceContext() { + com.google.api.gax.tracing.ApiTracer mockTracer = + Mockito.mock(com.google.api.gax.tracing.ApiTracer.class); + Mockito.doAnswer( + invocation -> { + java.util.Map carrier = invocation.getArgument(0); + carrier.put("traceparent", "00-00000000000000000000000000000001-0000000000000002-01"); + return null; + }) + .when(mockTracer) + .injectTraceContext(Mockito.anyMap()); + + java.util.Map> extraHeaders = new java.util.HashMap<>(); + extraHeaders.put("existing-header", java.util.Collections.singletonList("existing-value")); + + HttpJsonCallContext context = + (HttpJsonCallContext) + HttpJsonCallContext.createDefault() + .withTracer(mockTracer) + .withExtraHeaders(extraHeaders); + + HttpJsonMetadata metadata = HttpJsonClientCalls.getMetadataWithTraceContext(context); + + assertThat(metadata.getHeaders()).containsKey("existing-header"); + assertThat(metadata.getHeaders().get("existing-header").toString()).contains("existing-value"); + assertThat(metadata.getHeaders()).containsKey("traceparent"); + assertThat(metadata.getHeaders().get("traceparent").toString()) + .contains("00-00000000000000000000000000000001-0000000000000002-01"); + } } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpRequestRunnableTest.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpRequestRunnableTest.java index 27c7c3e470..72bf6a6d1b 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpRequestRunnableTest.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpRequestRunnableTest.java @@ -32,6 +32,7 @@ import com.google.api.client.http.EmptyContent; import com.google.api.client.http.HttpRequest; import com.google.api.client.testing.http.MockHttpTransport; +import com.google.api.gax.tracing.ApiTracer; import com.google.common.truth.Truth; import com.google.longrunning.ListOperationsRequest; import com.google.protobuf.Empty; @@ -123,6 +124,32 @@ void testRequestUrl() throws IOException { Truth.assertThat(httpRequest.getUrl().toString()).isEqualTo(expectedUrl); } + @Test + void testApiTracerRequestUrlResolved() throws IOException { + ApiTracer tracer = Mockito.mock(ApiTracer.class); + ApiMethodDescriptor methodDescriptor = + ApiMethodDescriptor.newBuilder() + .setFullMethodName("house.cat.get") + .setHttpMethod(null) + .setRequestFormatter(requestFormatter) + .setResponseParser(responseParser) + .build(); + + HttpRequestRunnable httpRequestRunnable = + new HttpRequestRunnable<>( + requestMessage, + methodDescriptor, + ENDPOINT, + HttpJsonCallOptions.newBuilder().setTracer(tracer).build(), + new MockHttpTransport(), + HttpJsonMetadata.newBuilder().build(), + (result) -> {}); + + httpRequestRunnable.createHttpRequest(); + String expectedUrl = ENDPOINT + "/name/feline" + "?food=bird&food=mouse&size=small"; + Mockito.verify(tracer).requestUrlResolved(expectedUrl); + } + @Test void testRequestUrlUnnormalized() throws IOException { ApiMethodDescriptor methodDescriptor = diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java index 931041201d..31427c8db1 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/MockHttpService.java @@ -43,6 +43,7 @@ import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; +import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; import java.util.Queue; @@ -261,7 +262,11 @@ public MockLowLevelHttpResponse getHttpResponse(String httpMethod, String fullTa httpContent = methodDescriptor.getResponseParser().serialize(response); } - httpResponse.setContent(httpContent.getBytes()); + byte[] contentBytes = httpContent.getBytes(StandardCharsets.UTF_8); + httpResponse.setContent(contentBytes); + if (methodDescriptor.getType() != MethodType.SERVER_STREAMING) { + httpResponse.addHeader("Content-Length", String.valueOf(contentBytes.length)); + } httpResponse.setStatusCode(200); return httpResponse; } diff --git a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java index 604e9ad47b..308dc4d602 100644 --- a/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java +++ b/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/testing/TestApiTracer.java @@ -32,6 +32,7 @@ import com.google.api.gax.tracing.ApiTracer; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.threeten.bp.Duration; /** @@ -43,6 +44,8 @@ public class TestApiTracer implements ApiTracer { private final AtomicInteger attemptsStarted = new AtomicInteger(); private final AtomicInteger attemptsFailed = new AtomicInteger(); private final AtomicBoolean retriesExhausted = new AtomicBoolean(false); + private final AtomicLong responseReceivedSize = new AtomicLong(); + private final AtomicInteger responsesReceived = new AtomicInteger(); public TestApiTracer() {} @@ -58,6 +61,14 @@ public AtomicBoolean getRetriesExhausted() { return retriesExhausted; } + public long getResponseReceivedSize() { + return responseReceivedSize.get(); + } + + public int getResponsesReceived() { + return responsesReceived.get(); + } + @Override public void attemptStarted(int attemptNumber) { attemptsStarted.incrementAndGet(); @@ -78,5 +89,38 @@ public void attemptFailedRetriesExhausted(Throwable error) { attemptsFailed.incrementAndGet(); retriesExhausted.set(true); } + + @Override + public void responseReceived() { + responsesReceived.incrementAndGet(); + } + + @Override + public void responseHeadersReceived(java.util.Map headers) { + long contentLength = extractContentLength(headers); + if (contentLength >= 0) { + responseReceivedSize.addAndGet(contentLength); + } + } + + private long extractContentLength(java.util.Map headers) { + if (headers == null || headers.isEmpty()) return -1; + Object value = + headers.entrySet().stream() + .filter(e -> "Content-Length".equalsIgnoreCase(e.getKey())) + .map(java.util.Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (value instanceof java.util.Collection) { + value = ((java.util.Collection) value).stream().findFirst().orElse(null); + } + + try { + return Long.parseLong(String.valueOf(value)); + } catch (NumberFormatException | NullPointerException e) { + return -1; + } + } } ; diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml index 7a03fad54d..9c2589279b 100644 --- a/gax-java/gax/pom.xml +++ b/gax-java/gax/pom.xml @@ -139,7 +139,7 @@ @{argLine} -Djava.util.logging.SimpleFormatter.format="%1$tY %1$tl:%1$tM:%1$tS.%1$tL %2$s %4$s: %5$s%6$s%n" - !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest + !EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,!LoggingEnabledTest,!LoggingTracerTest,!ClientContextTest#testGetApiTracerFactory_loggingEnabled @@ -154,7 +154,7 @@ org.apache.maven.plugins maven-surefire-plugin - EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest + EndpointContextTest#endpointContextBuild_universeDomainEnvVarSet+endpointContextBuild_multipleUniverseDomainConfigurations_clientSettingsHasPriority,LoggingEnabledTest,LoggingTracerTest, ClientContextTest#testGetApiTracerFactory_loggingEnabled diff --git a/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java index 0797527bc8..f3d504ff59 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/logging/LoggingUtils.java @@ -36,7 +36,7 @@ @InternalApi public class LoggingUtils { - static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; + private static final String GOOGLE_SDK_JAVA_LOGGING = "GOOGLE_SDK_JAVA_LOGGING"; private static boolean loggingEnabled = checkLoggingEnabled(GOOGLE_SDK_JAVA_LOGGING); @@ -45,7 +45,7 @@ public class LoggingUtils { * * @return true if logging is enabled, false otherwise. */ - static boolean isLoggingEnabled() { + public static boolean isLoggingEnabled() { return loggingEnabled; } 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 43fdd848b6..96e3131201 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 @@ -40,11 +40,13 @@ import com.google.api.gax.core.BackgroundResource; import com.google.api.gax.core.ExecutorAsBackgroundResource; import com.google.api.gax.core.ExecutorProvider; +import com.google.api.gax.logging.LoggingUtils; 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.api.gax.tracing.SpanTracerFactory; +import com.google.api.gax.tracing.CompositeTracerFactory; +import com.google.api.gax.tracing.LoggingTracerFactory; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -152,7 +154,8 @@ public static Builder newBuilder() { .setTracerFactory(BaseApiTracerFactory.getInstance()) .setQuotaProjectId(null) .setGdchApiAudience(null) - // Attempt to create an empty, non-functioning EndpointContext by default. This is + // Attempt to create an empty, non-functioning EndpointContext by default. This + // is // not exposed to the user via getters/setters. .setEndpointContext(EndpointContext.getDefaultInstance()); } @@ -185,8 +188,10 @@ public static ClientContext create(StubSettings settings) throws IOException { String settingsGdchApiAudience = settings.getGdchApiAudience(); boolean usingGDCH = credentials instanceof GdchCredentials; if (usingGDCH) { - // Can only determine if the GDC-H is being used via the Credentials. The Credentials object - // is resolved in the ClientContext and must be passed to the EndpointContext. Rebuild the + // Can only determine if the GDC-H is being used via the Credentials. The + // Credentials object + // is resolved in the ClientContext and must be passed to the EndpointContext. + // Rebuild the // endpointContext only on GDC-H flows. endpointContext = endpointContext.withGDCH(); // Resolve the new endpoint with the GDC-H flow @@ -199,16 +204,20 @@ public static ClientContext create(StubSettings settings) throws IOException { } if (settings.getQuotaProjectId() != null && credentials != null) { - // If the quotaProjectId is set, wrap original credentials with correct quotaProjectId as + // If the quotaProjectId is set, wrap original credentials with correct + // quotaProjectId as // QuotaProjectIdHidingCredentials. - // Ensure that a custom set quota project id takes priority over one detected by credentials. + // Ensure that a custom set quota project id takes priority over one detected by + // credentials. // Avoid the backend receiving possibly conflict values of quotaProjectId credentials = new QuotaProjectIdHidingCredentials(credentials); } TransportChannelProvider transportChannelProvider = settings.getTransportChannelProvider(); - // After needsExecutor and StubSettings#setExecutorProvider are deprecated, transport channel - // executor can only be set from TransportChannelProvider#withExecutor directly, and a provider + // After needsExecutor and StubSettings#setExecutorProvider are deprecated, + // transport channel + // executor can only be set from TransportChannelProvider#withExecutor directly, + // and a provider // will have a default executor if it needs one. if (transportChannelProvider.needsExecutor() && settings.getExecutorProvider() != null) { transportChannelProvider = @@ -271,16 +280,8 @@ public static ClientContext create(StubSettings settings) throws IOException { if (watchdogProvider != null && watchdogProvider.shouldAutoClose()) { backgroundResources.add(watchdog); } - ApiTracerContext apiTracerContext = - ApiTracerContext.newBuilder() - .setServerAddress(endpointContext.resolvedServerAddress()) - .setServerPort(endpointContext.resolvedServerPort()) - .setLibraryMetadata(settings.getLibraryMetadata()) - .build(); - ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); - if (apiTracerFactory instanceof SpanTracerFactory) { - apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); - } + + ApiTracerFactory apiTracerFactory = getApiTracerFactory(settings, endpointContext); return newBuilder() .setBackgroundResources(backgroundResources.build()) @@ -301,6 +302,32 @@ public static ClientContext create(StubSettings settings) throws IOException { .build(); } + @VisibleForTesting + static ApiTracerFactory getApiTracerFactory( + StubSettings settings, EndpointContext endpointContext) { + ApiTracerFactory apiTracerFactory = settings.getTracerFactory(); + + if (LoggingUtils.isLoggingEnabled()) { + apiTracerFactory = + new CompositeTracerFactory( + ImmutableList.of(new LoggingTracerFactory(), apiTracerFactory)); + } + + if (apiTracerFactory.needsContext()) { + ApiTracerContext apiTracerContext = + ApiTracerContext.newBuilder() + .setServerAddress(endpointContext.resolvedServerAddress()) + .setServerPort(endpointContext.resolvedServerPort()) + .setServiceName(endpointContext.serviceName()) + .setLibraryMetadata(settings.getLibraryMetadata()) + .setUrlDomain(endpointContext.getUrlDomain()) + .build(); + apiTracerFactory = apiTracerFactory.withContext(apiTracerContext); + } + + return apiTracerFactory; + } + /** Determines which credentials to use. API key overrides credentials provided by provider. */ private static Credentials getCredentials(StubSettings settings) throws IOException { Credentials credentials; 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 09e105cceb..1f408497dc 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 @@ -134,11 +134,20 @@ public static EndpointContext getDefaultInstance() { public abstract String resolvedEndpoint(); + @Nullable public abstract String resolvedServerAddress(); @Nullable public abstract Integer resolvedServerPort(); + @Nullable + String getUrlDomain() { + if (!Strings.isNullOrEmpty(serviceName()) && !Strings.isNullOrEmpty(resolvedUniverseDomain())) { + return serviceName() + "." + resolvedUniverseDomain(); + } + return null; + } + public abstract Builder toBuilder(); public static Builder newBuilder() { @@ -410,7 +419,7 @@ private Integer parseServerPort(String endpoint) { return null; } HostAndPort hostAndPort = parseServerHostAndPort(endpoint); - if (!hostAndPort.hasPort()) { + if (hostAndPort == null || !hostAndPort.hasPort()) { return null; } return hostAndPort.getPort(); @@ -466,8 +475,13 @@ public EndpointContext build() throws IOException { setResolvedUniverseDomain(determineUniverseDomain()); String endpoint = determineEndpoint(); setResolvedEndpoint(endpoint); - setResolvedServerAddress(parseServerAddress(resolvedEndpoint())); - setResolvedServerPort(parseServerPort(resolvedEndpoint())); + try { + setResolvedServerAddress(parseServerAddress(resolvedEndpoint())); + setResolvedServerPort(parseServerPort(resolvedEndpoint())); + } catch (Exception throwable) { + // Server address and server port are only used for observability. + // We should ignore any errors parsing them and not affect the main client requests. + } setUseS2A(shouldUseS2A()); return autoBuild(); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java index 97f8e017db..a67cdc8555 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ApiTracer.java @@ -179,6 +179,10 @@ default void lroStartSucceeded() {} default void responseReceived() {} ; + /** Adds an annotation that a streaming response has been received with its headers. */ + default void responseHeadersReceived(java.util.Map headers) {} + ; + /** Adds an annotation that a streaming request has been sent. */ default void requestSent() {} ; @@ -192,6 +196,17 @@ default void requestSent() {} default void batchRequestSent(long elementCount, long requestSize) {} ; + /** Extract the trace context from the tracer and add it to the given headers map. */ + default void injectTraceContext(java.util.Map carrier) {} + + /** + * Annotates the attempt with the full resolved HTTP URL. Only relevant for HTTP transport. + * + * @param requestUrl the full URL of the request + */ + default void requestUrlResolved(String requestUrl) {} + ; + /** * A context class to be used with {@link #inScope()} and a try-with-resources block. Closing a * {@link Scope} removes any context that the underlying implementation might've set in {@link 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 index d6f7566c74..068af97123 100644 --- 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 @@ -37,6 +37,7 @@ import com.google.common.base.Strings; import java.util.HashMap; import java.util.Map; +import java.util.function.Supplier; import javax.annotation.Nullable; /** @@ -164,9 +165,46 @@ String rpcSystemName() { @Nullable public abstract String urlDomain(); - /** The destination resource id of the request (e.g. projects/p/locations/l/topics/t). */ @Nullable - public abstract String destinationResourceId(); + protected abstract Supplier destinationResourceIdSupplier(); + + /** + * The destination resource id of the request (e.g. + * //pubsub.googleapis.com/projects/p/locations/l/topics/t). + */ + @Nullable + public String destinationResourceId() { + Supplier supplier = destinationResourceIdSupplier(); + if (supplier == null) { + return null; + } + String resourceId = supplier.get(); + if (Strings.isNullOrEmpty(resourceId)) { + return null; + } + if (Strings.isNullOrEmpty(urlDomain())) { + return resourceId; + } + return "//" + urlDomain() + "/" + resourceId; + } + + ApiTracerContext withResourceNameExtractor( + @Nullable RequestT request, + @Nullable com.google.api.gax.rpc.ResourceNameExtractor extractor) { + if (extractor == null || request == null) { + return this; + } + return toBuilder() + .setDestinationResourceIdSupplier( + () -> { + try { + return extractor.extract(request); + } catch (Exception e) { + return null; + } + }) + .build(); + } /** * @return a map of attributes to be included in attempt-level spans @@ -182,17 +220,8 @@ public Map getAttemptAttributes() { if (rpcSystemName() != null) { attributes.put(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, rpcSystemName()); } - if (!libraryMetadata().isEmpty()) { - if (!Strings.isNullOrEmpty(libraryMetadata().repository())) { - attributes.put(ObservabilityAttributes.REPO_ATTRIBUTE, libraryMetadata().repository()); - } - if (!Strings.isNullOrEmpty(libraryMetadata().artifactName())) { - attributes.put( - ObservabilityAttributes.ARTIFACT_ATTRIBUTE, libraryMetadata().artifactName()); - } - if (!Strings.isNullOrEmpty(libraryMetadata().version())) { - attributes.put(ObservabilityAttributes.VERSION_ATTRIBUTE, libraryMetadata().version()); - } + if (!libraryMetadata().isEmpty() && !Strings.isNullOrEmpty(libraryMetadata().repository())) { + attributes.put(ObservabilityAttributes.REPO_ATTRIBUTE, libraryMetadata().repository()); } if (transport() == Transport.GRPC && !Strings.isNullOrEmpty(fullMethodName())) { attributes.put(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE, fullMethodName()); @@ -205,10 +234,16 @@ public Map getAttemptAttributes() { attributes.put(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE, httpPathTemplate()); } } + if (!Strings.isNullOrEmpty(serviceName())) { + attributes.put(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE, serviceName()); + } if (!Strings.isNullOrEmpty(destinationResourceId())) { attributes.put( ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE, destinationResourceId()); } + if (!Strings.isNullOrEmpty(urlDomain())) { + attributes.put(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE, urlDomain()); + } return attributes; } @@ -220,6 +255,9 @@ Map getMetricsAttributes() { if (serverPort() != null) { attributes.put(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE, serverPort()); } + if (!libraryMetadata().isEmpty() && !Strings.isNullOrEmpty(libraryMetadata().repository())) { + attributes.put(ObservabilityAttributes.REPO_ATTRIBUTE, libraryMetadata().repository()); + } if (!Strings.isNullOrEmpty(serviceName())) { attributes.put(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE, serviceName()); } @@ -229,10 +267,10 @@ Map getMetricsAttributes() { if (!Strings.isNullOrEmpty(fullMethodName())) { attributes.put(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE, fullMethodName()); } + if (!Strings.isNullOrEmpty(urlDomain())) { + attributes.put(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE, urlDomain()); + } if (transport() == Transport.HTTP) { - if (!Strings.isNullOrEmpty(urlDomain())) { - attributes.put(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE, urlDomain()); - } if (!Strings.isNullOrEmpty(httpPathTemplate())) { attributes.put(ObservabilityAttributes.URL_TEMPLATE_ATTRIBUTE, httpPathTemplate()); } @@ -278,8 +316,8 @@ ApiTracerContext merge(ApiTracerContext other) { if (!Strings.isNullOrEmpty(other.urlDomain())) { builder.setUrlDomain(other.urlDomain()); } - if (other.destinationResourceId() != null) { - builder.setDestinationResourceId(other.destinationResourceId()); + if (other.destinationResourceIdSupplier() != null) { + builder.setDestinationResourceIdSupplier(other.destinationResourceIdSupplier()); } return builder.build(); } @@ -316,7 +354,8 @@ public abstract static class Builder { public abstract Builder setUrlDomain(@Nullable String urlDomain); - public abstract Builder setDestinationResourceId(@Nullable String destinationResourceId); + public abstract Builder setDestinationResourceIdSupplier( + @Nullable Supplier destinationResourceIdSupplier); 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 2d763440db..ee2bf66ab0 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 @@ -74,10 +74,14 @@ default ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { } /** - * @return the {@link ApiTracerContext} for this factory + * Indicates whether this factory requires an {@link ApiTracerContext} to be injected via {@link + * #withContext(ApiTracerContext)} before creating tracers. + * + * @return {@code true} if an {@link ApiTracerContext} should be injected, {@code false} + * otherwise. */ - default ApiTracerContext getApiTracerContext() { - return ApiTracerContext.empty(); + default boolean needsContext() { + return false; } /** diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java new file mode 100644 index 0000000000..ca08c20c86 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracer.java @@ -0,0 +1,223 @@ +/* + * 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.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * A composite implementation of {@link ApiTracer} that delegates all tracing events to a list of + * underlying tracers. + * + *

For internal use only. + */ +@InternalApi +class CompositeTracer extends BaseApiTracer { + private final List children; + + public CompositeTracer(List children) { + this.children = ImmutableList.copyOf(children); + } + + @Override + public Scope inScope() { + final List childScopes = new ArrayList<>(children.size()); + + try { + for (ApiTracer child : children) { + childScopes.add(child.inScope()); + } + } catch (RuntimeException e) { + for (int i = childScopes.size() - 1; i >= 0; i--) { + try { + childScopes.get(i).close(); + } catch (RuntimeException suppressed) { + e.addSuppressed(suppressed); + } + } + throw e; + } + + return () -> { + RuntimeException exception = null; + for (int i = childScopes.size() - 1; i >= 0; i--) { + try { + childScopes.get(i).close(); + } catch (RuntimeException e) { + if (exception == null) { + exception = e; + } else { + exception.addSuppressed(e); + } + } + } + if (exception != null) { + throw exception; + } + }; + } + + @Override + public void operationSucceeded() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationSucceeded(); + } + } + + @Override + public void operationCancelled() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationCancelled(); + } + } + + @Override + public void operationFailed(Throwable error) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).operationFailed(error); + } + } + + @Override + public void connectionSelected(String id) { + for (ApiTracer child : children) { + child.connectionSelected(id); + } + } + + @Override + @Deprecated + public void attemptStarted(int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(attemptNumber); + } + } + + @Override + public void attemptStarted(Object request, int attemptNumber) { + for (ApiTracer child : children) { + child.attemptStarted(request, attemptNumber); + } + } + + @Override + public void attemptSucceeded() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptSucceeded(); + } + } + + @Override + public void attemptCancelled() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptCancelled(); + } + } + + @Override + public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailed(error, delay); + } + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailedDuration(error, delay); + } + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptFailedRetriesExhausted(error); + } + } + + @Override + public void attemptPermanentFailure(Throwable error) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).attemptPermanentFailure(error); + } + } + + @Override + public void lroStartFailed(Throwable error) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).lroStartFailed(error); + } + } + + @Override + public void lroStartSucceeded() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).lroStartSucceeded(); + } + } + + @Override + public void responseReceived() { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).responseReceived(); + } + } + + @Override + public void responseHeadersReceived(Map headers) { + for (int i = children.size() - 1; i >= 0; i--) { + children.get(i).responseHeadersReceived(headers); + } + } + + @Override + public void requestSent() { + for (ApiTracer child : children) { + child.requestSent(); + } + } + + @Override + public void batchRequestSent(long elementCount, long requestSize) { + for (ApiTracer child : children) { + child.batchRequestSent(elementCount, requestSize); + } + } + + @Override + public void injectTraceContext(java.util.Map carrier) { + for (ApiTracer child : children) { + child.injectTraceContext(carrier); + } + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java new file mode 100644 index 0000000000..3e4c0041e1 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/CompositeTracerFactory.java @@ -0,0 +1,86 @@ +/* + * 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.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; + +/** + * A composite implementation of {@link ApiTracerFactory} that bundles multiple tracing factories + * and produces a {@link CompositeTracer} out of them. + */ +public class CompositeTracerFactory extends BaseApiTracerFactory { + private final List apiTracerFactories; + + public CompositeTracerFactory(List apiTracerFactories) { + this.apiTracerFactories = ImmutableList.copyOf(apiTracerFactories); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + List children = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + children.add(factory.newTracer(parent, spanName, operationType)); + } + return new CompositeTracer(children); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext tracerContext) { + List children = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + children.add(factory.newTracer(parent, tracerContext)); + } + return new CompositeTracer(children); + } + + @Override + public boolean needsContext() { + for (ApiTracerFactory factory : apiTracerFactories) { + if (factory.needsContext()) { + return true; + } + } + return false; + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + List contextualizedChildren = new ArrayList<>(apiTracerFactories.size()); + + for (ApiTracerFactory factory : apiTracerFactories) { + contextualizedChildren.add(factory.withContext(context)); + } + return new CompositeTracerFactory(contextualizedChildren); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java new file mode 100644 index 0000000000..4a345f3614 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ErrorTypeUtil.java @@ -0,0 +1,276 @@ +/* + * 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.gax.rpc.ApiException; +import com.google.api.gax.rpc.DeadlineExceededException; +import com.google.api.gax.rpc.WatchdogTimeoutException; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.UncheckedExecutionException; +import java.net.BindException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.nio.channels.UnresolvedAddressException; +import java.security.GeneralSecurityException; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.net.ssl.SSLHandshakeException; + +public class ErrorTypeUtil { + + enum ErrorType { + CLIENT_TIMEOUT, + CLIENT_CONNECTION_ERROR, + CLIENT_REQUEST_ERROR, + /** Placeholder for potential future request body errors. */ + CLIENT_REQUEST_BODY_ERROR, + /** Placeholder for potential future response decode errors. */ + CLIENT_RESPONSE_DECODE_ERROR, + /** Placeholder for potential future redirect errors. */ + CLIENT_REDIRECT_ERROR, + CLIENT_AUTHENTICATION_ERROR, + /** Placeholder for potential future unknown errors. */ + CLIENT_UNKNOWN_ERROR, + INTERNAL; + } + + private static final Set> AUTHENTICATION_EXCEPTION_CLASSES = + ImmutableSet.of(GeneralSecurityException.class); + + private static final Set> CLIENT_TIMEOUT_EXCEPTION_CLASSES = + ImmutableSet.of( + SocketTimeoutException.class, + WatchdogTimeoutException.class, + DeadlineExceededException.class); + + private static final Set> CLIENT_CONNECTION_EXCEPTIONS = + ImmutableSet.of( + ConnectException.class, + UnknownHostException.class, + SSLHandshakeException.class, + UnresolvedAddressException.class, + NoRouteToHostException.class, + BindException.class); + + /** + * Extracts a low-cardinality string representing the specific classification of the error to be + * used in the {@link ObservabilityAttributes#ERROR_TYPE_ATTRIBUTE} attribute. + * + *

This value is determined based on the following priority: + * + *

    + *
  1. {@code google.rpc.ErrorInfo.reason}: If the error response from the service + * includes {@code google.rpc.ErrorInfo} details, the reason field (e.g., + * "RATE_LIMIT_EXCEEDED", "SERVICE_DISABLED") will be used. This offers the most precise + * error cause. + *
  2. Specific Server Error Code: If no {@code ErrorInfo.reason} is available and it is + * not a client-side failure, but a server error code was received: + *
      + *
    • For HTTP: The HTTP status code (e.g., "403", "503"). + *
    • For gRPC: The gRPC status code name (e.g., "PERMISSION_DENIED", "UNAVAILABLE"). + *
    + *
  3. Client-Side Network/Operational Errors: For errors occurring within the client + * library or network stack, mapping to specific enum representations from {@link + * ErrorType}. This includes checking the exception for diagnostic markers (e.g., {@code + * ConnectException} or {@code SocketTimeoutException}). + *
  4. Language-specific error type: The class or struct name of the exception or error + * if available. This must be low-cardinality, meaning it returns the short name of the + * exception class (e.g. {@code "IllegalStateException"}) rather than its message. + *
  5. Internal Fallback: If the error doesn't fit any of the above categories, {@code + * "INTERNAL"} will be used, indicating an unexpected issue within the client library's own + * logic. + *
+ * + * @param error the Throwable from which to extract the error type string. + * @return a low-cardinality string representing the specific error type + */ + // Requirement source: go/clo:product-requirements-v1 + public static String extractErrorType(@Nonnull Throwable error) { + + // 1. Unwrap standard wrapper exceptions if present + Throwable realError = getRealCause(error); + + // 2. Attempt to extract specific error type from the main exception + String specificError = extractKnownErrorType(realError); + if (specificError != null) { + return specificError; + } + + // 3. Language-specific error type fallback + String exceptionName = realError.getClass().getSimpleName(); + if (!Strings.isNullOrEmpty(exceptionName)) { + return exceptionName; + } + + // 4. Internal Fallback + return ErrorType.INTERNAL.toString(); + } + + /** + * Unwraps standard execution wrappers to find the real cause of the failure. + * + *

This method specifically unwraps: + * + *

    + *
  • {@link com.google.common.util.concurrent.UncheckedExecutionException}: This is an + * unchecked exception often thrown by {@code ApiExceptions.callAndTranslateApiException} or + * {@code ServerStreamIterator} when a checked exception or error occurs. + *
+ * + * @param t the Throwable to unwrap. + * @return the cause of the exception if it is an instance of {@link UncheckedExecutionException} + * and has a cause; otherwise, the throwable itself. + */ + private static Throwable getRealCause(Throwable t) { + if (t.getCause() == null || !(t instanceof UncheckedExecutionException)) { + return t; + } + return t.getCause(); + } + + /** + * Attempts to extract a specific error type (reason, code, or client error) but returns null if + * it cannot be specifically classified. + */ + @Nullable + private static String extractKnownErrorType(Throwable error) { + // 1. Extract error info reason + if (error instanceof ApiException) { + String reason = ((ApiException) error).getReason(); + if (!Strings.isNullOrEmpty(reason)) { + return reason; + } + } + + // 2. Extract server status code (swapped order) + if (error instanceof ApiException) { + String errorCode = extractServerErrorCode((ApiException) error); + if (errorCode != null) { + return errorCode; + } + } + + // 3. Attempt client side error + String clientError = getClientSideError(error); + if (clientError != null) { + return clientError; + } + + return null; + } + + /** + * Extracts the server error code from an ApiException. + * + * @param apiException The ApiException to extract the error code from. + * @return A string representing the error code, or null if no specific code can be determined. + */ + @Nullable + private static String extractServerErrorCode(ApiException apiException) { + if (apiException.getStatusCode() != null) { + Object transportCode = apiException.getStatusCode().getTransportCode(); + if (transportCode != null) { + return String.valueOf(transportCode); + } + } + return null; + } + + /** + * Determines the client-side error type based on the provided Throwable. This method checks for + * various network and client-specific exceptions. + * + * @param error The Throwable to analyze. + * @return A string representing the client-side error type, or null if not matched. + */ + @Nullable + private static String getClientSideError(Throwable error) { + if (isClientTimeout(error)) { + return ErrorType.CLIENT_TIMEOUT.toString(); + } + if (isClientConnectionError(error)) { + return ErrorType.CLIENT_CONNECTION_ERROR.toString(); + } + if (isClientAuthenticationError(error)) { + return ErrorType.CLIENT_AUTHENTICATION_ERROR.toString(); + } + // This covers CLIENT_REQUEST_ERROR for general illegal arguments in client requests. + if (error instanceof IllegalArgumentException) { + return ErrorType.CLIENT_REQUEST_ERROR.toString(); + } + return null; + } + + /** + * Checks if the given Throwable represents a client-side timeout error. This includes socket + * timeouts and GAX-specific watchdog timeouts. + * + * @param e The Throwable to check. + * @return true if the error is a client timeout, false otherwise. + */ + private static boolean isClientTimeout(Throwable e) { + return hasErrorClass(e, CLIENT_TIMEOUT_EXCEPTION_CLASSES); + } + + /** + * Checks if the given Throwable represents a client-side connection error. This includes issues + * with establishing connections, unknown hosts, SSL handshakes, and unresolved addresses. + * + * @param e The Throwable to check. + * @return true if the error is a client connection error, false otherwise. + */ + private static boolean isClientConnectionError(Throwable e) { + return hasErrorClass(e, CLIENT_CONNECTION_EXCEPTIONS); + } + + private static boolean isClientAuthenticationError(Throwable e) { + return hasErrorClass(e, AUTHENTICATION_EXCEPTION_CLASSES); + } + + /** + * Checks if the throwable is an instance of any of the specified error classes. + * + * @param t The Throwable to check. + * @param errorClasses A set of class objects to check against. + * @return true if the error is an instance of a class from the set, false otherwise. + */ + private static boolean hasErrorClass(Throwable t, Set> errorClasses) { + for (Class errorClass : errorClasses) { + if (errorClass.isInstance(t)) { + return true; + } + } + return false; + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java index 299a2cdb33..19fec5879c 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java @@ -29,12 +29,16 @@ */ package com.google.api.gax.tracing; +import com.google.api.gax.rpc.LibraryMetadata; +import com.google.common.base.Strings; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; import java.util.Arrays; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; /** * This class takes an OpenTelemetry object, and creates instruments (meters, histograms etc.) from @@ -53,8 +57,23 @@ class GoldenSignalsMetricsRecorder { 900.0, 3600.0); final DoubleHistogram clientRequestDurationRecorder; - GoldenSignalsMetricsRecorder(OpenTelemetry openTelemetry, String libraryName) { - Meter meter = openTelemetry.meterBuilder(libraryName).build(); + @Nullable + static GoldenSignalsMetricsRecorder create( + OpenTelemetry openTelemetry, LibraryMetadata libraryMetadata) { + if (libraryMetadata == null || Strings.isNullOrEmpty(libraryMetadata.artifactName())) { + return null; + } + return new GoldenSignalsMetricsRecorder(openTelemetry, libraryMetadata); + } + + private GoldenSignalsMetricsRecorder( + OpenTelemetry openTelemetry, LibraryMetadata libraryMetadata) { + MeterBuilder meterBuilder = openTelemetry.meterBuilder(libraryMetadata.artifactName()); + String libraryVersion = libraryMetadata.version(); + if (!Strings.isNullOrEmpty(libraryVersion)) { + meterBuilder.setInstrumentationVersion(libraryVersion); + } + Meter meter = meterBuilder.build(); this.clientRequestDurationRecorder = meter diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.java new file mode 100644 index 0000000000..552ad2659b --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracer.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 com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.logging.LoggerProvider; +import com.google.api.gax.logging.LoggingUtils; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; +import com.google.rpc.ErrorInfo; +import java.util.HashMap; +import java.util.Map; + +/** + * An {@link ApiTracer} that logs actionable errors using {@link LoggingUtils} when an RPC attempt + * fails. + */ +@BetaApi +@InternalApi +class LoggingTracer extends BaseApiTracer { + private static final LoggerProvider LOGGER_PROVIDER = + LoggerProvider.forClazz(LoggingTracer.class); + + private final ApiTracerContext apiTracerContext; + + LoggingTracer(ApiTracerContext apiTracerContext) { + this.apiTracerContext = apiTracerContext; + } + + @Override + public void attemptFailedDuration(Throwable error, java.time.Duration delay) { + recordActionableError(error); + } + + @Override + public void attemptFailedRetriesExhausted(Throwable error) { + recordActionableError(error); + } + + @Override + public void attemptPermanentFailure(Throwable error) { + recordActionableError(error); + } + + @VisibleForTesting + void recordActionableError(Throwable error) { + if (error == null) { + return; + } + + Map logContext = new HashMap<>(apiTracerContext.getAttemptAttributes()); + logContext.putAll( + ObservabilityUtils.getResponseAttributes(error, apiTracerContext.transport())); + + if (!Strings.isNullOrEmpty(error.getMessage())) { + logContext.put(ObservabilityAttributes.EXCEPTION_MESSAGE_ATTRIBUTE, error.getMessage()); + } + + ErrorInfo errorInfo = ObservabilityUtils.extractErrorInfo(error); + if (errorInfo != null) { + if (!Strings.isNullOrEmpty(errorInfo.getDomain())) { + logContext.put(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, errorInfo.getDomain()); + } + if (errorInfo.getMetadataMap() != null) { + for (Map.Entry entry : errorInfo.getMetadataMap().entrySet()) { + logContext.put( + ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + entry.getKey(), + entry.getValue()); + } + } + } + + String message = error.getMessage() != null ? error.getMessage() : error.getClass().getName(); + LoggingUtils.logActionableError(logContext, LOGGER_PROVIDER, message); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java similarity index 58% rename from gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java rename to gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java index f891795564..8f75cded24 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracerFactory.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/LoggingTracerFactory.java @@ -32,63 +32,50 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; +import com.google.api.gax.logging.LoggingUtils; import com.google.common.annotations.VisibleForTesting; -import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.trace.Tracer; /** - * A {@link ApiTracerFactory} to build instances of {@link SpanTracer}. - * - *

This class wraps the {@link Tracer} and pass it to {@link SpanTracer}. It will be used to - * record traces in {@link SpanTracer}. - * - *

This class is expected to be initialized once during client initialization. + * A {@link ApiTracerFactory} that creates instances of {@link LoggingTracer}. This class is + * intended for internal framework use only. Manual instantiation is discouraged; the lifecycle is + * managed automatically by the system, when {@link LoggingUtils#isLoggingEnabled()} returning + * {@code true}. */ @BetaApi @InternalApi -public class SpanTracerFactory implements ApiTracerFactory { - private final Tracer tracer; - +public class LoggingTracerFactory implements ApiTracerFactory { private final ApiTracerContext apiTracerContext; - /** Creates a SpanTracerFactory */ - public SpanTracerFactory(OpenTelemetry openTelemetry) { - this(openTelemetry.getTracer("gax-java"), ApiTracerContext.empty()); + public LoggingTracerFactory() { + this(ApiTracerContext.empty()); } - /** - * Pass in a Map of client level attributes which will be added to every single SpanTracer created - * from the ApiTracerFactory. This is package private since span attributes are determined - * internally. - */ - @VisibleForTesting - SpanTracerFactory(Tracer tracer, ApiTracerContext apiTracerContext) { - this.tracer = tracer; + private LoggingTracerFactory(ApiTracerContext apiTracerContext) { this.apiTracerContext = apiTracerContext; } @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"; - - return new SpanTracer(tracer, this.apiTracerContext, attemptSpanName); + return new LoggingTracer(apiTracerContext); } @Override - public ApiTracer newTracer(ApiTracer parent, ApiTracerContext apiTracerContext) { - ApiTracerContext mergedContext = this.apiTracerContext.merge(apiTracerContext); - return new SpanTracer(tracer, mergedContext); + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext context) { + return new LoggingTracer(apiTracerContext.merge(context)); } - @Override - public ApiTracerContext getApiTracerContext() { + @VisibleForTesting + ApiTracerContext getApiTracerContext() { return apiTracerContext; } + @Override + public boolean needsContext() { + return apiTracerContext == null || apiTracerContext.equals(ApiTracerContext.empty()); + } + @Override public ApiTracerFactory withContext(ApiTracerContext context) { - return new SpanTracerFactory(tracer, apiTracerContext.merge(context)); + return new LoggingTracerFactory(apiTracerContext.merge(context)); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java index e9ad908c21..13aac89862 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java @@ -120,7 +120,8 @@ public void operationFailed(Throwable error) { if (operationFinished.getAndSet(true)) { throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); } - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + // Uses the GRPC status code representation. + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordOperationCount(1, attributes); @@ -172,7 +173,7 @@ public void attemptCancelled() { */ @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -196,7 +197,7 @@ public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { */ @Override public void attemptFailedRetriesExhausted(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -210,7 +211,7 @@ public void attemptFailedRetriesExhausted(Throwable error) { */ @Override public void attemptPermanentFailure(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error).toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java index 64095fde00..99981bd469 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityAttributes.java @@ -50,12 +50,6 @@ public class ObservabilityAttributes { /** The repository of the client library (e.g., "googleapis/google-cloud-java"). */ public static final String REPO_ATTRIBUTE = "gcp.client.repo"; - /** The artifact name of the client library (e.g., "google-cloud-vision"). */ - public static final String ARTIFACT_ATTRIBUTE = "gcp.client.artifact"; - - /** The version of the client library (e.g., "1.2.3"). */ - public static final String VERSION_ATTRIBUTE = "gcp.client.version"; - /** The full RPC method name, including package, service, and method. */ public static final String GRPC_RPC_METHOD_ATTRIBUTE = "rpc.method"; @@ -85,6 +79,21 @@ public class ObservabilityAttributes { /** The url template of the request (e.g. /v1/{name}:access). */ public static final String URL_TEMPLATE_ATTRIBUTE = "url.template"; + /** A human-readable error message, which may include details from the exception or response. */ + public static final String STATUS_MESSAGE_ATTRIBUTE = "status.message"; + + /** If the error was caused by an exception, the exception class name. */ + public static final String EXCEPTION_TYPE_ATTRIBUTE = "exception.type"; + + /** If the error was caused by an exception, the exception message. */ + public static final String EXCEPTION_MESSAGE_ATTRIBUTE = "exception.message"; + + /** Size of the response body in bytes. */ + public static final String HTTP_RESPONSE_BODY_SIZE = "http.response.body.size"; + + /** The HTTP status code of the request (e.g., 200, 404). */ + public static final String HTTP_RESPONSE_STATUS_ATTRIBUTE = "http.response.status_code"; + /** The resend count of the request. Only used in HTTP transport. */ public static final String HTTP_RESEND_COUNT_ATTRIBUTE = "http.request.resend_count"; @@ -93,4 +102,19 @@ public class ObservabilityAttributes { /** The destination resource id of the request (e.g. projects/p/locations/l/topics/t). */ public static final String DESTINATION_RESOURCE_ID_ATTRIBUTE = "gcp.resource.destination.id"; + + /** The full URL of the HTTP request, with sensitive query parameters redacted. */ + public static final String HTTP_URL_FULL_ATTRIBUTE = "url.full"; + + /** + * * The specific error type. Value will be google.rpc.ErrorInfo.reason, a specific Server Error + * Code, Client-Side Network/Operational Error (e.g., CLIENT_TIMEOUT) or internal fallback. + */ + public static final String ERROR_TYPE_ATTRIBUTE = "error.type"; + + /** The domain of the error (e.g., from google.rpc.ErrorInfo.domain). */ + public static final String ERROR_DOMAIN_ATTRIBUTE = "gcp.errors.domain"; + + /** The prefix for error metadata (e.g., from google.rpc.ErrorInfo.metadata). */ + public static final String ERROR_METADATA_ATTRIBUTE_PREFIX = "gcp.errors.metadata."; } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index 2487964370..dfdfc4ccb4 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -31,29 +31,164 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.google.rpc.ErrorInfo; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.CancellationException; import javax.annotation.Nullable; -class ObservabilityUtils { - - /** Function to extract the status of the error as a string */ - static String extractStatus(@Nullable Throwable error) { - final String statusString; +final class ObservabilityUtils { + /** Function to extract the status of the error as a canonical code. */ + static StatusCode.Code extractStatus(@Nullable Throwable error) { if (error == null) { - return StatusCode.Code.OK.toString(); + return StatusCode.Code.OK; } else if (error instanceof CancellationException) { - statusString = StatusCode.Code.CANCELLED.toString(); + return StatusCode.Code.CANCELLED; } else if (error instanceof ApiException) { - statusString = ((ApiException) error).getStatusCode().getCode().toString(); + return ((ApiException) error).getStatusCode().getCode(); } else { - statusString = StatusCode.Code.UNKNOWN.toString(); + return StatusCode.Code.UNKNOWN; + } + } + + /** Constant for redacted values. */ + private static final String REDACTED_VALUE = "REDACTED"; + + /** + * A set of lowercase query parameter keys whose values should be redacted in URLs for + * observability. These include direct credentials (access keys), cryptographic signatures (to + * prevent replay attacks or leak of authorization), and session identifiers (like upload_id). + */ + private static final ImmutableSet SENSITIVE_QUERY_KEYS = + ImmutableSet.of( + "awsaccesskeyid", // AWS S3-compatible access keys + "signature", // General cryptographic signature + "sig", // General cryptographic signature (abbreviated) + "x-goog-signature", // Google Cloud specific signature + "upload_id", // Resumable upload session identifiers + "access_token", // OAuth2 explicit tokens + "key", // API Keys + "api_key"); // API Keys + + /** + * Sanitizes an HTTP URL by redacting sensitive query parameters and credentials in the user-info + * component. If the provided URL cannot be parsed (e.g. invalid syntax), it returns the original + * string. + * + *

This sanitization process conforms to the recommendations in footnote 3 of the OpenTelemetry + * semantic conventions for HTTP URL attributes: + * https://opentelemetry.io/docs/specs/semconv/registry/attributes/url/ + * + *

    + *
  • "url.full MUST NOT contain credentials passed via URL in form of + * https://user:pass@example.com/. In such case username and password SHOULD be redacted and + * attribute's value SHOULD be https://REDACTED:REDACTED@example.com/." - Handled by + * stripping the raw user info component. + *
  • "url.full SHOULD capture the absolute URL when it is available (or can be + * reconstructed)." - Handled by parsing and rebuilding the generic URI. + *
  • "When a query string value is redacted, the query string key SHOULD still be + * preserved, e.g. https://www.example.com/path?color=blue&sig=REDACTED." - Handled by + * the redactSensitiveQueryValues method. + *
+ * + * @param url the raw URL string + * @return the sanitized URL string, or the original if unparsable + */ + static String sanitizeUrlFull(final String url) { + try { + java.net.URI uri = new java.net.URI(url); + String sanitizedUserInfo = + uri.getRawUserInfo() != null ? REDACTED_VALUE + ":" + REDACTED_VALUE : null; + String sanitizedQuery = redactSensitiveQueryValues(uri.getRawQuery()); + java.net.URI sanitizedUri = + new java.net.URI( + uri.getScheme(), + sanitizedUserInfo, + uri.getHost(), + uri.getPort(), + uri.getRawPath(), + sanitizedQuery, + uri.getRawFragment()); + return sanitizedUri.toString(); + } catch (java.net.URISyntaxException | IllegalArgumentException ex) { + return ""; + } + } + + /** + * Redacts the values of sensitive keys within a raw URI query string. + * + *

This logic splits the query string by the {@code &} delimiter without full URL decoding, + * ensures only values belonging to predefined sensitive keys are replaced with {@code + * REDACTED_VALUE}. The check is strictly case-insensitive. + * + *

Note regarding Footnote 3: The OpenTelemetry spec recommends case-sensitive matching for + * query parameters. However, we intentionally utilize case-insensitive matching (by lowercasing + * all query keys) to prevent credentials bypassing validation when sent with mixed casings (e.g., + * Sig=..., API_KEY=...). + * + * @param rawQuery the raw query string from a java.net.URI + * @return a reconstructed query sequence with sensitive values redacted + */ + private static String redactSensitiveQueryValues(final String rawQuery) { + if (rawQuery == null || rawQuery.isEmpty()) { + return rawQuery; } - return statusString; + java.util.List redactedParams = + Splitter.on('&').splitToList(rawQuery).stream() + .map( + param -> { + int equalsIndex = param.indexOf('='); + if (equalsIndex < 0) { + return param; + } + String key = param.substring(0, equalsIndex); + // Case-insensitive match utilizing the fact that all + // predefined keys are in lowercase + if (SENSITIVE_QUERY_KEYS.contains(key.toLowerCase())) { + return key + "=" + REDACTED_VALUE; + } + return param; + }) + .collect(java.util.stream.Collectors.toList()); + + return Joiner.on('&').join(redactedParams); + } + + static Map getResponseAttributes( + @Nullable Throwable error, ApiTracerContext.Transport transport) { + Map attributes = new HashMap<>(); + StatusCode.Code code = extractStatus(error); + attributes.put(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, code.toString()); + if (transport == ApiTracerContext.Transport.HTTP) { + attributes.put( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, (long) code.getHttpStatusCode()); + } + if (error != null) { + attributes.put( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ErrorTypeUtil.extractErrorType(error)); + attributes.put(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE, error.getClass().getName()); + } + return attributes; + } + + /** Function to extract the ErrorInfo payload from the error, if available */ + @Nullable + static ErrorInfo extractErrorInfo(@Nullable Throwable error) { + if (error instanceof ApiException) { + ApiException apiException = (ApiException) error; + if (apiException.getErrorDetails() != null) { + return apiException.getErrorDetails().getErrorInfo(); + } + } + return null; } static Attributes toOtelAttributes(Map attributes) { diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactory.java similarity index 79% rename from gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java rename to gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactory.java index 53b8998a86..dceabeba2a 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactory.java @@ -34,19 +34,19 @@ import io.opentelemetry.api.OpenTelemetry; /** - * A {@link ApiTracerFactory} to build instances of {@link GoldenSignalsMetricsTracer}. + * A {@link ApiTracerFactory} to build instances of {@link OpenTelemetryMetricsTracer}. * *

This class is expected to be initialized once during client initialization. */ @BetaApi @InternalApi -public class GoldenSignalsMetricsTracerFactory implements ApiTracerFactory { +public class OpenTelemetryMetricsFactory implements ApiTracerFactory { private ApiTracerContext clientLevelTracerContext; private final OpenTelemetry openTelemetry; private GoldenSignalsMetricsRecorder metricsRecorder; - public GoldenSignalsMetricsTracerFactory(OpenTelemetry openTelemetry) { + public OpenTelemetryMetricsFactory(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; this.clientLevelTracerContext = ApiTracerContext.empty(); } @@ -58,7 +58,7 @@ public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType op // regular requests. return new BaseApiTracer(); } - return new GoldenSignalsMetricsTracer(metricsRecorder, clientLevelTracerContext); + return new OpenTelemetryMetricsTracer(metricsRecorder, clientLevelTracerContext); } @Override @@ -69,15 +69,26 @@ public ApiTracer newTracer(ApiTracer parent, ApiTracerContext methodLevelTracerC return new BaseApiTracer(); } ApiTracerContext mergedTracerContext = clientLevelTracerContext.merge(methodLevelTracerContext); - return new GoldenSignalsMetricsTracer(metricsRecorder, mergedTracerContext); + return new OpenTelemetryMetricsTracer(metricsRecorder, mergedTracerContext); + } + + @Override + public boolean needsContext() { + return clientLevelTracerContext == null + || clientLevelTracerContext.equals(ApiTracerContext.empty()); } @Override public ApiTracerFactory withContext(ApiTracerContext context) { - this.clientLevelTracerContext = context; + if (context == null) { + return new BaseApiTracerFactory(); + } this.metricsRecorder = - new GoldenSignalsMetricsRecorder( - openTelemetry, clientLevelTracerContext.libraryMetadata().artifactName()); + GoldenSignalsMetricsRecorder.create(openTelemetry, context.libraryMetadata()); + if (this.metricsRecorder == null) { + return new BaseApiTracerFactory(); + } + this.clientLevelTracerContext = context; return this; } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracer.java similarity index 81% rename from gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java rename to gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracer.java index 21d9d7093b..dfcf1b1d4a 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracer.java @@ -29,14 +29,12 @@ */ package com.google.api.gax.tracing; -import static com.google.api.gax.tracing.ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE; - -import com.google.api.gax.rpc.StatusCode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; /** @@ -45,26 +43,29 @@ * GoldenSignalsMetricsRecorder}, hence this class should not have any knowledge about the * observability framework (e.g. OpenTelemetry). */ -class GoldenSignalsMetricsTracer implements ApiTracer { +class OpenTelemetryMetricsTracer implements ApiTracer { private final Stopwatch clientRequestTimer; private final GoldenSignalsMetricsRecorder metricsRecorder; private final Map attributes; + private final ApiTracerContext.Transport transport; - GoldenSignalsMetricsTracer( + OpenTelemetryMetricsTracer( GoldenSignalsMetricsRecorder metricsRecorder, ApiTracerContext apiTracerContext) { this.clientRequestTimer = Stopwatch.createStarted(); this.metricsRecorder = metricsRecorder; this.attributes = apiTracerContext.getMetricsAttributes(); + this.transport = apiTracerContext.transport(); } @VisibleForTesting - GoldenSignalsMetricsTracer( + OpenTelemetryMetricsTracer( GoldenSignalsMetricsRecorder metricsRecorder, ApiTracerContext apiTracerContext, Ticker ticker) { this.clientRequestTimer = Stopwatch.createStarted(ticker); this.metricsRecorder = metricsRecorder; this.attributes = new HashMap<>(apiTracerContext.getMetricsAttributes()); + this.transport = apiTracerContext.transport(); } /** @@ -74,21 +75,23 @@ class GoldenSignalsMetricsTracer implements ApiTracer { */ @Override public void operationSucceeded() { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.OK.toString()); - metricsRecorder.recordOperationLatency( - clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); + recordMetric(null); } @Override public void operationCancelled() { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString()); - metricsRecorder.recordOperationLatency( - clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); + recordMetric(new CancellationException()); } @Override public void operationFailed(Throwable error) { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + recordMetric(error); + } + + private void recordMetric(Throwable error) { + Map responseAttributes = + ObservabilityUtils.getResponseAttributes(error, transport); + attributes.putAll(responseAttributes); metricsRecorder.recordOperationLatency( clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingFactory.java new file mode 100644 index 0000000000..ad148e5268 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingFactory.java @@ -0,0 +1,130 @@ +/* + * 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.client.util.Strings; +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import com.google.api.gax.rpc.LibraryMetadata; +import com.google.common.annotations.VisibleForTesting; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; + +/** + * A {@link ApiTracerFactory} to build instances of {@link OpenTelemetryTracingTracer}. + * + *

This class wraps the {@link Tracer} 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 OpenTelemetryTracingFactory implements ApiTracerFactory { + private final Tracer tracer; + private final OpenTelemetry openTelemetry; + private final ApiTracerContext apiTracerContext; + + /** + * Warning: Traces may contain sensitive data such as resource names, full URLs, and error + * messages. + * + *

Before configuring subscribers or exporters for traces, review the contents of the spans and + * consult the + * OpenTelemetry documentation to set up filters and formatters to prevent leaking sensitive + * information, depending on your intended use case. + * + *

See also the + * OpenTelemetry Semantic Conventions. + */ + public OpenTelemetryTracingFactory(OpenTelemetry openTelemetry) { + this(openTelemetry, null, ApiTracerContext.empty()); + } + + /** + * Pass in a Map of client level attributes which will be added to every single + * OpenTelemetryTracingTracer created from the ApiTracerFactory. This is package private since + * span attributes are determined internally. + */ + @VisibleForTesting + OpenTelemetryTracingFactory( + OpenTelemetry openTelemetry, Tracer tracer, ApiTracerContext apiTracerContext) { + this.openTelemetry = openTelemetry; + this.tracer = tracer; + this.apiTracerContext = apiTracerContext; + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + if (tracer == null) { + // Return a no-op tracer if withContext hasn't been called to initialize the tracer properly + return new BaseApiTracer(); + } + // 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"; + + return new OpenTelemetryTracingTracer(tracer, this.apiTracerContext, attemptSpanName); + } + + @Override + public ApiTracer newTracer(ApiTracer parent, ApiTracerContext apiTracerContext) { + if (tracer == null) { + // Return a no-op tracer if withContext hasn't been called to initialize the tracer properly + return new BaseApiTracer(); + } + ApiTracerContext mergedContext = this.apiTracerContext.merge(apiTracerContext); + return new OpenTelemetryTracingTracer(tracer, mergedContext); + } + + @Override + public boolean needsContext() { + return apiTracerContext == null || apiTracerContext.equals(ApiTracerContext.empty()); + } + + /** + * Returns a new OpenTelemetryTracingFactory with the provided context. The Tracer is + * re-initialized using the artifact name and version from the library metadata. + */ + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + if (context == null) { + return new BaseApiTracerFactory(); + } + LibraryMetadata metadata = context.libraryMetadata(); + if (metadata == null || metadata.isEmpty() || Strings.isNullOrEmpty(metadata.artifactName())) { + return new BaseApiTracerFactory(); + } + Tracer newTracer = openTelemetry.getTracer(metadata.artifactName(), metadata.version()); + ApiTracerContext mergedContext = this.apiTracerContext.merge(context); + return new OpenTelemetryTracingFactory(openTelemetry, newTracer, mergedContext); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java similarity index 57% rename from gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java rename to gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java index a2690359dd..549b3b4ab7 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/SpanTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryTracingTracer.java @@ -30,6 +30,7 @@ package com.google.api.gax.tracing; +import com.google.api.client.util.Strings; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import io.opentelemetry.api.trace.Span; @@ -38,14 +39,14 @@ import io.opentelemetry.api.trace.Tracer; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; /** An implementation of {@link ApiTracer} that uses OpenTelemetry to record traces. */ @BetaApi @InternalApi -public class SpanTracer implements ApiTracer { - public static final String LANGUAGE_ATTRIBUTE = "gcp.client.language"; +class OpenTelemetryTracingTracer implements ApiTracer { - public static final String DEFAULT_LANGUAGE = "Java"; + static final String CONTENT_LENGTH_KEY = "Content-Length"; private final Tracer tracer; private final Map attemptAttributes; @@ -53,13 +54,30 @@ public class SpanTracer implements ApiTracer { private final ApiTracerContext apiTracerContext; private Span attemptSpan; + @Override + public void injectTraceContext(java.util.Map carrier) { + if (attemptSpan != null) { + io.opentelemetry.context.Context context = + io.opentelemetry.context.Context.current().with(attemptSpan); + io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator.getInstance() + .inject( + context, + carrier, + (c, k, v) -> { + if (c != null) { + c.put(k, v); + } + }); + } + } + /** - * Creates a new instance of {@code SpanTracer}. + * Creates a new instance of {@code OpenTelemetryTracingTracer}. * * @param tracer the {@link Tracer} to use for recording spans * @param apiTracerContext the {@link ApiTracerContext} to use for recording spans */ - public SpanTracer(Tracer tracer, ApiTracerContext apiTracerContext) { + OpenTelemetryTracingTracer(Tracer tracer, ApiTracerContext apiTracerContext) { this.tracer = tracer; this.apiTracerContext = apiTracerContext; this.attemptSpanName = resolveAttemptSpanName(apiTracerContext); @@ -68,14 +86,16 @@ public SpanTracer(Tracer tracer, ApiTracerContext apiTracerContext) { } /** - * Creates a new instance of {@code SpanTracer} with an explicitly provided span name. + * Creates a new instance of {@code OpenTelemetryTracingTracer} with an explicitly provided span + * name. * * @param tracer the {@link Tracer} to use for recording spans * @param apiTracerContext the {@link ApiTracerContext} to use for recording spans * @param attemptSpanName the name of the individual attempt spans */ @InternalApi - SpanTracer(Tracer tracer, ApiTracerContext apiTracerContext, String attemptSpanName) { + OpenTelemetryTracingTracer( + Tracer tracer, ApiTracerContext apiTracerContext, String attemptSpanName) { this.tracer = tracer; this.attemptSpanName = attemptSpanName; this.apiTracerContext = apiTracerContext; @@ -99,7 +119,6 @@ private static String resolveAttemptSpanName(ApiTracerContext apiTracerContext) } private void buildAttributes() { - this.attemptAttributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE); this.attemptAttributes.putAll(this.apiTracerContext.getAttemptAttributes()); } @@ -131,33 +150,107 @@ public void attemptStarted(Object request, int attemptNumber) { @Override public void attemptSucceeded() { - endAttempt(); + recordErrorAndEndAttempt(null); + } + + @Override + public void responseHeadersReceived(java.util.Map headers) { + if (attemptSpan == null) { + return; + } + long contentLength = extractContentLength(headers); + if (contentLength >= 0) { + attemptSpan.setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, contentLength); + } + } + + /** + * Extracts the Content-Length header value from the response headers, if available. + * + *

Note: google-http-java-client's HttpHeaders.java returns some headers (like Content-Length) + * as a List instead of a single value. + * https://github.com/googleapis/google-http-java-client/blob/main/google-http-client/src/main/java/com/google/api/client/http/HttpHeaders.java#L162 + * + * @param headers the map of response headers. + * @return the content length in bytes, or -1 if the header is missing or malformed. + */ + private long extractContentLength(java.util.Map headers) { + try { + if (headers == null || headers.isEmpty()) return -1; + // google-http-client HttpHeaders uses a case-insensitive map but we copy it for safety + // and to handle potential different implementations. + Object value = + headers.entrySet().stream() + .filter(e -> CONTENT_LENGTH_KEY.equalsIgnoreCase(e.getKey())) + .map(Map.Entry::getValue) + .findFirst() + .orElse(null); + + if (value instanceof java.util.Collection) { + value = ((java.util.Collection) value).stream().findFirst().orElse(null); + } + return Long.parseLong(value.toString()); + } catch (Exception e) { + return -1; + } } @Override public void attemptCancelled() { - endAttempt(); + recordErrorAndEndAttempt(new CancellationException()); } @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - endAttempt(); + recordErrorAndEndAttempt(error); } @Override public void attemptFailedRetriesExhausted(Throwable error) { - endAttempt(); + recordErrorAndEndAttempt(error); } @Override public void attemptPermanentFailure(Throwable error) { + recordErrorAndEndAttempt(error); + } + + private void recordErrorAndEndAttempt(Throwable error) { + if (attemptSpan == null) { + return; + } + Map responseAttributes = + ObservabilityUtils.getResponseAttributes(error, this.apiTracerContext.transport()); + if (!responseAttributes.isEmpty()) { + attemptSpan.setAllAttributes(ObservabilityUtils.toOtelAttributes(responseAttributes)); + } + + if (error != null && !Strings.isNullOrEmpty(error.getMessage())) { + attemptSpan.setAttribute( + ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, error.getMessage()); + } + endAttempt(); } private void endAttempt() { - if (attemptSpan != null) { - attemptSpan.end(); - attemptSpan = null; + if (attemptSpan == null) { + return; + } + + attemptSpan.end(); + attemptSpan = null; + } + + @Override + public void requestUrlResolved(String url) { + if (attemptSpan == null) { + return; + } + String sanitizedUrlString = ObservabilityUtils.sanitizeUrlFull(url); + if (sanitizedUrlString.isEmpty()) { + return; } + attemptSpan.setAttribute(ObservabilityAttributes.HTTP_URL_FULL_ATTRIBUTE, sanitizedUrlString); } } diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java index 99fe33c99f..7c8a4f71c3 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java @@ -37,8 +37,6 @@ import com.google.api.gax.rpc.ResourceNameExtractor; import com.google.api.gax.rpc.UnaryCallable; import com.google.api.gax.tracing.ApiTracerFactory.OperationType; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.util.concurrent.MoreExecutors; import javax.annotation.Nullable; @@ -90,8 +88,10 @@ public TracedUnaryCallable( public ApiFuture futureCall(RequestT request, ApiCallContext context) { ApiTracer tracer; if (apiTracerContext != null) { - ApiTracerContext finalContext = extractResourceNameToApiTracerContext(request); - tracer = tracerFactory.newTracer(context.getTracer(), finalContext); + tracer = + tracerFactory.newTracer( + context.getTracer(), + apiTracerContext.withResourceNameExtractor(request, resourceNameExtractor)); } else { tracer = tracerFactory.newTracer(context.getTracer(), spanName, OperationType.Unary); } @@ -108,15 +108,4 @@ public ApiFuture futureCall(RequestT request, ApiCallContext context) throw e; } } - - @VisibleForTesting - ApiTracerContext extractResourceNameToApiTracerContext(RequestT request) { - ApiTracerContext finalContext = apiTracerContext; - String resourceName = - resourceNameExtractor != null ? resourceNameExtractor.extract(request) : null; - if (!Strings.isNullOrEmpty(resourceName)) { - finalContext = finalContext.toBuilder().setDestinationResourceId(resourceName).build(); - } - return finalContext; - } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java index 3ee7e513cd..36e8d47e48 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestLogger.java @@ -48,8 +48,20 @@ public class TestLogger implements Logger, LoggingEventAware { List messageList = new ArrayList<>(); Level level; + public List getMessageList() { + return messageList; + } + Map keyValuePairsMap = new HashMap<>(); + public Map getMDCMap() { + return MDCMap; + } + + public Map getKeyValuePairsMap() { + return keyValuePairsMap; + } + private String loggerName; private boolean infoEnabled; private boolean debugEnabled; diff --git a/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java index 69cc43ba2a..2b7f0f3b82 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/logging/TestServiceProvider.java @@ -30,12 +30,11 @@ package com.google.api.gax.logging; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.slf4j.ILoggerFactory; import org.slf4j.IMarkerFactory; +import org.slf4j.Logger; import org.slf4j.spi.MDCAdapter; import org.slf4j.spi.SLF4JServiceProvider; @@ -45,12 +44,18 @@ */ public class TestServiceProvider implements SLF4JServiceProvider { + private final ConcurrentMap loggers = new ConcurrentHashMap<>(); + private final ILoggerFactory loggerFactory = + new ILoggerFactory() { + @Override + public Logger getLogger(String name) { + return loggers.computeIfAbsent(name, TestLogger::new); + } + }; + @Override public ILoggerFactory getLoggerFactory() { - // mock behavior when provider present - ILoggerFactory mockLoggerFactory = mock(ILoggerFactory.class); - when(mockLoggerFactory.getLogger(anyString())).thenReturn(new TestLogger("test-logger")); - return mockLoggerFactory; + return loggerFactory; } @Override diff --git a/gax-java/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java b/gax-java/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java index bdb89f1943..fb9e2f17f3 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/retrying/RetryAlgorithmTest.java @@ -64,7 +64,9 @@ void testCreateFirstAttemptWithUnusedContext() { void testCreateFirstAttemptWithContext() { TimedRetryAlgorithmWithContext timedAlgorithm = mock(TimedRetryAlgorithmWithContext.class); RetryAlgorithm algorithm = - new RetryAlgorithm<>(mock(ResultRetryAlgorithmWithContext.class), timedAlgorithm); + new RetryAlgorithm<>( + (ResultRetryAlgorithmWithContext) mock(ResultRetryAlgorithmWithContext.class), + (TimedRetryAlgorithmWithContext) timedAlgorithm); RetryingContext context = mock(RetryingContext.class); algorithm.createFirstAttempt(context); diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java index d3c202d99e..9852f04cdb 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/ClientContextTest.java @@ -54,7 +54,6 @@ import com.google.api.gax.rpc.testing.FakeStubSettings; import com.google.api.gax.rpc.testing.FakeTransportChannel; import com.google.api.gax.tracing.ApiTracerFactory; -import com.google.api.gax.tracing.SpanTracerFactory; import com.google.auth.ApiKeyCredentials; import com.google.auth.CredentialTypeForMetrics; import com.google.auth.Credentials; @@ -1297,7 +1296,8 @@ void testCreate_withTracerFactoryReturningNullWithContext() throws IOException { builder.setCredentialsProvider( FixedCredentialsProvider.create(Mockito.mock(Credentials.class))); - ApiTracerFactory apiTracerFactory = Mockito.mock(SpanTracerFactory.class); + ApiTracerFactory apiTracerFactory = Mockito.mock(ApiTracerFactory.class); + Mockito.doReturn(true).when(apiTracerFactory).needsContext(); Mockito.doReturn(apiTracerFactory).when(apiTracerFactory).withContext(Mockito.any()); FakeStubSettings settings = Mockito.spy(builder.build()); @@ -1307,4 +1307,68 @@ void testCreate_withTracerFactoryReturningNullWithContext() throws IOException { assertThat(context.getTracerFactory()).isSameInstanceAs(apiTracerFactory); verify(apiTracerFactory, times(1)).withContext(Mockito.any()); } + + @Test + void testGetApiTracerFactory_noContextNeeded() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(false); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(mockTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory).isSameInstanceAs(mockTracerFactory); + } + + @Test + void testGetApiTracerFactory_contextNeeded() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + ApiTracerFactory withContextTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(true); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(withContextTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + when(endpointContext.resolvedServerAddress()).thenReturn("test-address"); + when(endpointContext.resolvedServerPort()).thenReturn(443); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory).isSameInstanceAs(withContextTracerFactory); + verify(mockTracerFactory, times(1)) + .withContext(Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class)); + } + + // This test should only run when the maven profile `EnvVarTest` is enabled. + @Test + void testGetApiTracerFactory_loggingEnabled() throws java.io.IOException { + ApiTracerFactory mockTracerFactory = Mockito.mock(ApiTracerFactory.class); + when(mockTracerFactory.needsContext()).thenReturn(false); + when(mockTracerFactory.withContext( + Mockito.any(com.google.api.gax.tracing.ApiTracerContext.class))) + .thenReturn(mockTracerFactory); + + FakeStubSettings.Builder builder = FakeStubSettings.newBuilder(); + builder.setTracerFactory(mockTracerFactory); + + EndpointContext endpointContext = Mockito.mock(EndpointContext.class); + + ApiTracerFactory apiTracerFactory = + ClientContext.getApiTracerFactory(builder.build(), endpointContext); + + assertThat(apiTracerFactory) + .isInstanceOf(com.google.api.gax.tracing.CompositeTracerFactory.class); + } } 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 99a8f63fcf..a86286f880 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 @@ -683,4 +683,44 @@ void endpointContextBuild_resolvesPort() throws IOException { .build(); Truth.assertThat(endpointContext.resolvedServerPort()).isNull(); } + + @Test + void endpointContextBuild_resolvesInvalidEndpointAndPort() throws Exception { + + String endpoint = "localhost:-1"; + + EndpointContext endpointContext = + defaultEndpointContextBuilder + .setClientSettingsEndpoint(endpoint) + .setTransportChannelProviderEndpoint(null) + .build(); + + Truth.assertThat(endpointContext.resolvedServerAddress()).isNull(); + Truth.assertThat(endpointContext.resolvedServerPort()).isNull(); + } + + @Test + void getUrlDomain_success() throws IOException { + EndpointContext endpointContext = defaultEndpointContextBuilder.build(); + Truth.assertThat(endpointContext.getUrlDomain()).isEqualTo("test.googleapis.com"); + } + + @Test + void getUrlDomain_nullServiceName() throws IOException { + EndpointContext endpointContext = defaultEndpointContextBuilder.setServiceName(null).build(); + Truth.assertThat(endpointContext.getUrlDomain()).isNull(); + } + + @Test + void getUrlDomain_emptyServiceName() throws IOException { + EndpointContext endpointContext = defaultEndpointContextBuilder.setServiceName("").build(); + Truth.assertThat(endpointContext.getUrlDomain()).isNull(); + } + + @Test + void getUrlDomain_nonGDUUniverseDomain() throws IOException { + EndpointContext endpointContext = + defaultEndpointContextBuilder.setUniverseDomain("random.com").build(); + Truth.assertThat(endpointContext.getUrlDomain()).isEqualTo("test.random.com"); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java b/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java index 4004a24404..39ba0053bd 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/rpc/testing/FakeStubSettings.java @@ -32,6 +32,7 @@ import com.google.api.core.InternalApi; import com.google.api.gax.core.GoogleCredentialsProvider; import com.google.api.gax.rpc.ClientContext; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.rpc.StubSettings; import com.google.common.collect.ImmutableList; import java.io.IOException; @@ -55,6 +56,11 @@ public String getServiceName() { return "test"; } + @Override + protected LibraryMetadata getLibraryMetadata() { + return LibraryMetadata.newBuilder().build(); + } + @Override public StubSettings.Builder toBuilder() { return new Builder(this); 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 index b465dfbebe..9c0c01e544 100644 --- 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 @@ -62,28 +62,6 @@ void testGetAttemptAttributes_repo() { assertThat(attributes).containsEntry(ObservabilityAttributes.REPO_ATTRIBUTE, "test-repo"); } - @Test - void testGetAttemptAttributes_artifact() { - LibraryMetadata libraryMetadata = - LibraryMetadata.newBuilder().setArtifactName("test-artifact").build(); - ApiTracerContext context = - ApiTracerContext.newBuilder().setLibraryMetadata(libraryMetadata).build(); - Map attributes = context.getAttemptAttributes(); - - assertThat(attributes) - .containsEntry(ObservabilityAttributes.ARTIFACT_ATTRIBUTE, "test-artifact"); - } - - @Test - void testGetAttemptAttributes_version() { - LibraryMetadata libraryMetadata = LibraryMetadata.newBuilder().setVersion("1.2.3").build(); - ApiTracerContext context = - ApiTracerContext.newBuilder().setLibraryMetadata(libraryMetadata).build(); - Map attributes = context.getAttemptAttributes(); - - assertThat(attributes).containsEntry(ObservabilityAttributes.VERSION_ATTRIBUTE, "1.2.3"); - } - @Test void testGetAttemptAttributes_httpMethod() { ApiTracerContext context = @@ -186,6 +164,32 @@ void testGetAttemptAttributes_emptyStrings() { assertThat(attributes).isEmpty(); } + @Test + void testGetAttemptAttributes_urlDomain() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(LibraryMetadata.empty()) + .setUrlDomain("test-domain.com") + .build(); + Map attributes = context.getAttemptAttributes(); + + assertThat(attributes) + .containsEntry(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE, "test-domain.com"); + } + + @Test + void testGetAttemptAttributes_serviceName() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(LibraryMetadata.empty()) + .setServiceName("test-service") + .build(); + Map attributes = context.getAttemptAttributes(); + + assertThat(attributes) + .containsEntry(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE, "test-service"); + } + @Test void testGetMetricsAttributes_serverPort() { ApiTracerContext context = @@ -284,7 +288,7 @@ void testGetAttemptAttributes_destinationResourceId() { ApiTracerContext context = ApiTracerContext.newBuilder() .setLibraryMetadata(LibraryMetadata.empty()) - .setDestinationResourceId("projects/123/instances/abc") + .setDestinationResourceIdSupplier(() -> "projects/123/instances/abc") .build(); Map attributes = context.getAttemptAttributes(); @@ -304,7 +308,8 @@ void testGetMetricsAttributes_urlDomain_notHttp() { .build(); Map attributes = context.getMetricsAttributes(); - assertThat(attributes).doesNotContainKey(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE, "test-domain.com"); } @Test @@ -356,13 +361,13 @@ void testMerge_destinationResourceId() { ApiTracerContext context1 = ApiTracerContext.newBuilder() .setLibraryMetadata(LibraryMetadata.empty()) - .setDestinationResourceId("name1") + .setDestinationResourceIdSupplier(() -> "name1") .build(); ApiTracerContext context2 = ApiTracerContext.newBuilder() .setLibraryMetadata(LibraryMetadata.empty()) - .setDestinationResourceId("name2") + .setDestinationResourceIdSupplier(() -> "name2") .build(); ApiTracerContext merged = context1.merge(context2); @@ -407,4 +412,121 @@ void testMerge_httpFields() { assertThat(merged.httpMethod()).isEqualTo("GET"); assertThat(merged.httpPathTemplate()).isEqualTo("v1/projects/{project}"); } + + @Test + void testDestinationResourceId_withUrlDomain() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(LibraryMetadata.empty()) + .setUrlDomain("compute.googleapis.com") + .setDestinationResourceIdSupplier(() -> "projects/my-project/locations/us-central1") + .build(); + + assertThat(context.destinationResourceId()) + .isEqualTo("//compute.googleapis.com/projects/my-project/locations/us-central1"); + } + + @Test + void testDestinationResourceId_withoutUrlDomain() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(LibraryMetadata.empty()) + .setDestinationResourceIdSupplier(() -> "projects/my-project/locations/us-central1") + .build(); + + assertThat(context.destinationResourceId()) + .isEqualTo("projects/my-project/locations/us-central1"); + } + + @Test + void testWithResourceNameExtractor_nullExtractor() { + ApiTracerContext context = ApiTracerContext.empty(); + ApiTracerContext result = context.withResourceNameExtractor("request", null); + assertThat(result).isSameInstanceAs(context); + } + + @Test + void testWithResourceNameExtractor_extractorReturnsNull() { + ApiTracerContext context = ApiTracerContext.empty(); + ApiTracerContext result = context.withResourceNameExtractor("request", req -> null); + assertThat(result.destinationResourceId()).isNull(); + } + + @Test + void testWithResourceNameExtraction_lazyExtractor() { + ApiTracerContext context = ApiTracerContext.empty(); + boolean[] extracted = {false}; + ApiTracerContext result = + context.withResourceNameExtractor( + "request", + req -> { + extracted[0] = true; + return "extracted-id"; + }); + + assertThat(extracted[0]).isFalse(); // Should be lazily evaluated + assertThat(result.destinationResourceId()).isEqualTo("extracted-id"); + assertThat(extracted[0]).isTrue(); + } + + @Test + void testWithResourceNameExtractor_extractorThrowsException() { + ApiTracerContext context = ApiTracerContext.empty(); + ApiTracerContext result = + context.withResourceNameExtractor( + "request", + req -> { + throw new RuntimeException("Intentional mock extraction failure"); + }); + + assertThat(result.destinationResourceId()).isNull(); + } + + @Test + void testDestinationResourceId_emptyIdWithUrlDomain() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(LibraryMetadata.empty()) + .setUrlDomain("compute.googleapis.com") + .setDestinationResourceIdSupplier(() -> "") + .build(); + + assertThat(context.destinationResourceId()).isNull(); + } + + @Test + void testMerge_preservesLaziness() { + ApiTracerContext context1 = ApiTracerContext.empty(); + boolean[] extracted = {false}; + ApiTracerContext context2 = + ApiTracerContext.empty() + .withResourceNameExtractor( + "request", + req -> { + extracted[0] = true; + return "lazy-id"; + }); + + ApiTracerContext merged = context1.merge(context2); + assertThat(extracted[0]).isFalse(); // Should not be evaluated during merge + assertThat(merged.destinationResourceId()).isEqualTo("lazy-id"); + assertThat(extracted[0]).isTrue(); // Evaluated upon calling getter + } + + @Test + void testDestinationResourceId_evaluatedEveryTime() { + ApiTracerContext context = ApiTracerContext.empty(); + int[] counter = {0}; + ApiTracerContext result = + context.withResourceNameExtractor( + "request", + req -> { + counter[0]++; + return "extracted-id-" + counter[0]; + }); + + assertThat(result.destinationResourceId()).isEqualTo("extracted-id-1"); + assertThat(result.destinationResourceId()).isEqualTo("extracted-id-2"); + assertThat(counter[0]).isEqualTo(2); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java new file mode 100644 index 0000000000..d508a3e388 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerFactoryTest.java @@ -0,0 +1,147 @@ +/* + * 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.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class CompositeTracerFactoryTest { + + private ApiTracerFactory childFactory1; + private ApiTracerFactory childFactory2; + private CompositeTracerFactory compositeFactory; + + @BeforeEach + void setUp() { + childFactory1 = mock(ApiTracerFactory.class); + childFactory2 = mock(ApiTracerFactory.class); + compositeFactory = new CompositeTracerFactory(Arrays.asList(childFactory1, childFactory2)); + } + + @Test + void testNewTracerWithParentAndSpanName() { + ApiTracer parent = mock(ApiTracer.class); + SpanName spanName = SpanName.of("TestClient", "TestMethod"); + ApiTracerFactory.OperationType operationType = ApiTracerFactory.OperationType.Unary; + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(childFactory1.newTracer(parent, spanName, operationType)).thenReturn(tracer1); + when(childFactory2.newTracer(parent, spanName, operationType)).thenReturn(tracer2); + + ApiTracer compositeTracer = compositeFactory.newTracer(parent, spanName, operationType); + + // Verify that the composite delegates operation succeeded to its internal children + compositeTracer.operationSucceeded(); + + verify(childFactory1).newTracer(parent, spanName, operationType); + verify(childFactory2).newTracer(parent, spanName, operationType); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testNewTracerWithApiTracerContext() { + ApiTracer parent = mock(ApiTracer.class); + ApiTracerContext context = ApiTracerContext.empty(); + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(childFactory1.newTracer(parent, context)).thenReturn(tracer1); + when(childFactory2.newTracer(parent, context)).thenReturn(tracer2); + + ApiTracer compositeTracer = compositeFactory.newTracer(parent, context); + + // Verify that the composite delegates correctly + compositeTracer.operationSucceeded(); + + verify(childFactory1).newTracer(parent, context); + verify(childFactory2).newTracer(parent, context); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testWithContext() { + ApiTracerContext context = ApiTracerContext.empty(); + + ApiTracerFactory contextualizedFactory1 = mock(ApiTracerFactory.class); + ApiTracerFactory contextualizedFactory2 = mock(ApiTracerFactory.class); + + when(childFactory1.withContext(context)).thenReturn(contextualizedFactory1); + when(childFactory2.withContext(context)).thenReturn(contextualizedFactory2); + + ApiTracerFactory newCompositeFactory = compositeFactory.withContext(context); + + // Create tracer from the new compositeFactory and verify it delegates to the contextualized + // children + ApiTracer parent = mock(ApiTracer.class); + ApiTracerContext tracerContext = ApiTracerContext.empty(); + + ApiTracer tracer1 = mock(ApiTracer.class); + ApiTracer tracer2 = mock(ApiTracer.class); + + when(contextualizedFactory1.newTracer(parent, tracerContext)).thenReturn(tracer1); + when(contextualizedFactory2.newTracer(parent, tracerContext)).thenReturn(tracer2); + + ApiTracer compositeTracer = newCompositeFactory.newTracer(parent, tracerContext); + compositeTracer.operationSucceeded(); + + verify(childFactory1).withContext(context); + verify(childFactory2).withContext(context); + verify(contextualizedFactory1).newTracer(parent, tracerContext); + verify(contextualizedFactory2).newTracer(parent, tracerContext); + verify(tracer1).operationSucceeded(); + verify(tracer2).operationSucceeded(); + } + + @Test + void testNeedsContext_returnsFalseWhenNoChildrenNeedContext() { + when(childFactory1.needsContext()).thenReturn(false); + when(childFactory2.needsContext()).thenReturn(false); + + org.junit.jupiter.api.Assertions.assertFalse(compositeFactory.needsContext()); + } + + @Test + void testNeedsContext_returnsTrueWhenAtLeastOneChildNeedsContext() { + when(childFactory1.needsContext()).thenReturn(true); + when(childFactory2.needsContext()).thenReturn(false); + + org.junit.jupiter.api.Assertions.assertTrue(compositeFactory.needsContext()); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java new file mode 100644 index 0000000000..abb32e2618 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/CompositeTracerTest.java @@ -0,0 +1,282 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +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.Arrays; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +class CompositeTracerTest { + + private ApiTracer child1; + private ApiTracer child2; + private CompositeTracer compositeTracer; + + @BeforeEach + void setUp() { + child1 = mock(ApiTracer.class); + child2 = mock(ApiTracer.class); + compositeTracer = new CompositeTracer(Arrays.asList(child1, child2)); + } + + @Test + void testInScope_lifoOrder() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + ApiTracer.Scope scope2 = mock(ApiTracer.Scope.class); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenReturn(scope2); + + ApiTracer.Scope compositeScope = compositeTracer.inScope(); + compositeScope.close(); + + verify(child1).inScope(); + verify(child2).inScope(); + + InOrder inOrder = inOrder(scope2, scope1); + inOrder.verify(scope2).close(); + inOrder.verify(scope1).close(); + } + + @Test + void testInScope_childInScopeThrows() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + RuntimeException exception = new RuntimeException("Runtime Error"); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenThrow(exception); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> compositeTracer.inScope()); + + assertEquals(exception, thrown); + verify(child1).inScope(); + verify(child2).inScope(); + verify(scope1).close(); + } + + @Test + void testInScope_childScopeCloseThrows() { + ApiTracer.Scope scope1 = mock(ApiTracer.Scope.class); + ApiTracer.Scope scope2 = mock(ApiTracer.Scope.class); + + RuntimeException exception2 = new RuntimeException("Scope 2 close Error"); + RuntimeException exception1 = new RuntimeException("Scope 1 close Error"); + + when(child1.inScope()).thenReturn(scope1); + when(child2.inScope()).thenReturn(scope2); + + doThrow(exception2).when(scope2).close(); + doThrow(exception1).when(scope1).close(); + + ApiTracer.Scope compositeScope = compositeTracer.inScope(); + + RuntimeException thrown = assertThrows(RuntimeException.class, () -> compositeScope.close()); + + assertEquals(exception2, thrown); + assertTrue(Arrays.asList(thrown.getSuppressed()).contains(exception1)); + + InOrder inOrder = inOrder(scope2, scope1); + inOrder.verify(scope2).close(); + inOrder.verify(scope1).close(); + } + + @Test + void testOperationSucceeded() { + compositeTracer.operationSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationSucceeded(); + inOrder.verify(child1).operationSucceeded(); + } + + @Test + void testOperationCancelled() { + compositeTracer.operationCancelled(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationCancelled(); + inOrder.verify(child1).operationCancelled(); + } + + @Test + void testOperationFailed() { + Throwable error = new RuntimeException("test error"); + compositeTracer.operationFailed(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).operationFailed(error); + inOrder.verify(child1).operationFailed(error); + } + + @Test + void testConnectionSelected() { + String id = "connection_id_1"; + compositeTracer.connectionSelected(id); + verify(child1).connectionSelected(id); + verify(child2).connectionSelected(id); + } + + @Test + @SuppressWarnings("deprecation") + void testAttemptStartedDeprecated() { + compositeTracer.attemptStarted(2); + verify(child1).attemptStarted(2); + verify(child2).attemptStarted(2); + } + + @Test + void testAttemptStarted() { + Object request = new Object(); + compositeTracer.attemptStarted(request, 3); + verify(child1).attemptStarted(request, 3); + verify(child2).attemptStarted(request, 3); + } + + @Test + void testAttemptSucceeded() { + compositeTracer.attemptSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptSucceeded(); + inOrder.verify(child1).attemptSucceeded(); + } + + @Test + void testAttemptCancelled() { + compositeTracer.attemptCancelled(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptCancelled(); + inOrder.verify(child1).attemptCancelled(); + } + + @Test + @SuppressWarnings("deprecation") + void testAttemptFailedDeprecated() { + Throwable error = new RuntimeException("test error"); + org.threeten.bp.Duration delay = org.threeten.bp.Duration.ofSeconds(1); + compositeTracer.attemptFailed(error, delay); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailed(error, delay); + inOrder.verify(child1).attemptFailed(error, delay); + } + + @Test + void testAttemptFailedDuration() { + Throwable error = new RuntimeException("test error"); + java.time.Duration delay = java.time.Duration.ofSeconds(1); + compositeTracer.attemptFailedDuration(error, delay); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailedDuration(error, delay); + inOrder.verify(child1).attemptFailedDuration(error, delay); + } + + @Test + void testAttemptFailedRetriesExhausted() { + Throwable error = new RuntimeException("test error"); + compositeTracer.attemptFailedRetriesExhausted(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptFailedRetriesExhausted(error); + inOrder.verify(child1).attemptFailedRetriesExhausted(error); + } + + @Test + void testAttemptPermanentFailure() { + Throwable error = new RuntimeException("test error"); + compositeTracer.attemptPermanentFailure(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).attemptPermanentFailure(error); + inOrder.verify(child1).attemptPermanentFailure(error); + } + + @Test + void testLroStartFailed() { + Throwable error = new RuntimeException("test error"); + compositeTracer.lroStartFailed(error); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).lroStartFailed(error); + inOrder.verify(child1).lroStartFailed(error); + } + + @Test + void testLroStartSucceeded() { + compositeTracer.lroStartSucceeded(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).lroStartSucceeded(); + inOrder.verify(child1).lroStartSucceeded(); + } + + @Test + void testResponseReceived() { + compositeTracer.responseReceived(); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).responseReceived(); + inOrder.verify(child1).responseReceived(); + } + + @Test + void testResponseHeadersReceived() { + Map headers = ImmutableMap.of("testHeader", "testValue"); + compositeTracer.responseHeadersReceived(headers); + InOrder inOrder = inOrder(child2, child1); + inOrder.verify(child2).responseHeadersReceived(headers); + inOrder.verify(child1).responseHeadersReceived(headers); + } + + @Test + void testRequestSent() { + compositeTracer.requestSent(); + verify(child1).requestSent(); + verify(child2).requestSent(); + } + + @Test + void testBatchRequestSent() { + compositeTracer.batchRequestSent(10L, 100L); + verify(child1).batchRequestSent(10L, 100L); + verify(child2).batchRequestSent(10L, 100L); + } + + @Test + void testInjectTraceContext() { + java.util.Map carrier = new java.util.HashMap<>(); + compositeTracer.injectTraceContext(carrier); + InOrder inOrder = inOrder(child1, child2); + inOrder.verify(child1).injectTraceContext(carrier); + inOrder.verify(child2).injectTraceContext(carrier); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/ErrorTypeUtilTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ErrorTypeUtilTest.java new file mode 100644 index 0000000000..e59c7f5e51 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ErrorTypeUtilTest.java @@ -0,0 +1,198 @@ +/* + * 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 com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.DeadlineExceededException; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.common.util.concurrent.UncheckedExecutionException; +import java.io.IOException; +import java.net.BindException; +import java.net.ConnectException; +import java.net.InetSocketAddress; +import java.net.NoRouteToHostException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import javax.net.ssl.SSLHandshakeException; +import org.junit.jupiter.api.Test; + +class ErrorTypeUtilTest { + + @Test + void testExtractErrorType_apiException_noReason() { + ApiException exception = + new ApiException( + "fake_error", null, new FakeStatusCode(StatusCode.Code.INVALID_ARGUMENT), false); + assertThat(ErrorTypeUtil.extractErrorType(exception)) + .isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString()); + } + + @Test + void testExtractErrorType_realSocketTimeoutException() throws Exception { + try (ServerSocket serverSocket = new ServerSocket(0)) { + int port = serverSocket.getLocalPort(); + try (Socket clientSocket = new Socket()) { + clientSocket.connect(new InetSocketAddress("localhost", port), 1000); + clientSocket.setSoTimeout(10); // 10ms read timeout + clientSocket.getInputStream().read(); + org.junit.jupiter.api.Assertions.fail("Expected SocketTimeoutException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(SocketTimeoutException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + } + } + } + + @Test + void testExtractErrorType_realConnectException() { + try { + try (ServerSocket tempServer = new ServerSocket(0)) { + int freePort = tempServer.getLocalPort(); + tempServer.close(); + new Socket("localhost", freePort); + } + org.junit.jupiter.api.Assertions.fail("Expected ConnectException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(ConnectException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + + @Test + void testExtractErrorType_realUnknownHostException() { + try { + new Socket("this.host.does.not.exist.invalid", 80); + org.junit.jupiter.api.Assertions.fail("Expected UnknownHostException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(UnknownHostException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + + @Test + void testExtractErrorType_realSSLHandshakeException() throws Exception { + // Emulating a reliable SSLHandshakeException (vs a generic SSLException) requires + // complex keystore setups which are brittle. We instantiate it directly here. + assertThat( + ErrorTypeUtil.extractErrorType(new SSLHandshakeException("Cert path building failed"))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + + @Test + void testExtractErrorType_realBindException() throws Exception { + try (ServerSocket serverSocket1 = new ServerSocket(0)) { + int port = serverSocket1.getLocalPort(); + try (ServerSocket serverSocket2 = new ServerSocket(port)) { + org.junit.jupiter.api.Assertions.fail("Expected BindException"); + } catch (Exception e) { + assertThat(e).isInstanceOf(BindException.class); + assertThat(ErrorTypeUtil.extractErrorType(e)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + } + } + + @Test + void testExtractErrorType_clientTimeout_others() { + assertThat( + ErrorTypeUtil.extractErrorType( + new DeadlineExceededException( + "timeout", null, new FakeStatusCode(StatusCode.Code.DEADLINE_EXCEEDED), false))) + .isEqualTo(StatusCode.Code.DEADLINE_EXCEEDED.toString()); + } + + @Test + void testExtractErrorType_clientAuthenticationError() { + assertThat(ErrorTypeUtil.extractErrorType(new GeneralSecurityException("auth fail"))) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_AUTHENTICATION_ERROR.toString()); + } + + @Test + void testExtractErrorType_clientRequestError() { + assertThat(ErrorTypeUtil.extractErrorType(new IllegalArgumentException())) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_REQUEST_ERROR.toString()); + } + + @Test + void testExtractErrorType_fallbackToSimpleName() { + assertThat(ErrorTypeUtil.extractErrorType(new NullPointerException())) + .isEqualTo("NullPointerException"); + assertThat(ErrorTypeUtil.extractErrorType(new IllegalStateException())) + .isEqualTo("IllegalStateException"); + } + + @Test + void testExtractErrorType_otherNetworkErrors() { + assertThat(ErrorTypeUtil.extractErrorType(new NoRouteToHostException())) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + } + + @Test + void testExtractErrorType_noCauseChainTraversal() { + Exception root = new ConnectException("refused"); + Exception wrapped = new IOException("io fail", root); + assertThat(ErrorTypeUtil.extractErrorType(wrapped)).isEqualTo("IOException"); + } + + @Test + void testExtractErrorType_unknownException() { + assertThat(ErrorTypeUtil.extractErrorType(new Exception("Unknown stuff"))) + .isEqualTo("Exception"); + } + + @Test + void testExtractErrorType_redirectFallback() { + assertThat(ErrorTypeUtil.extractErrorType(new Exception("redirect"))).isEqualTo("Exception"); + } + + @Test + void testExtractErrorType_unknownClassNameFallback() { + class UnknownClientException extends Exception {} + assertThat(ErrorTypeUtil.extractErrorType(new UnknownClientException())) + .isEqualTo("UnknownClientException"); + } + + @Test + void testExtractErrorType_uncheckedExecutionException_unwraps() { + Exception cause = new SocketTimeoutException("timeout"); + Exception wrapper = new UncheckedExecutionException(cause); + assertThat(ErrorTypeUtil.extractErrorType(wrapper)) + .isEqualTo(ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java index 3476e64a72..51191671a6 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java @@ -34,6 +34,7 @@ import static com.google.api.gax.tracing.GoldenSignalsMetricsRecorder.CLIENT_REQUEST_DURATION_METRIC_NAME; import static com.google.common.truth.Truth.assertThat; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; @@ -62,7 +63,13 @@ void setUp() { SdkMeterProvider.builder().registerMetricReader(metricReader).build(); OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); - recorder = new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME); + recorder = + GoldenSignalsMetricsRecorder.create( + openTelemetry, + LibraryMetadata.newBuilder() + .setArtifactName(ARTIFACT_NAME) + .setVersion("1.2.3") + .build()); } @Test @@ -114,4 +121,27 @@ void recordOperationLatency_shouldRecordMetricAttributes() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo(Attributes.of(AttributeKey.stringKey(ATTRIBUTE_1), VALUE_1)); } + + @Test + void create_shouldReturnNull_whenLibraryMetadataIsNull() { + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), null); + assertThat(actual).isNull(); + } + + @Test + void create_shouldReturnNull_whenArtifactNameIsNull() { + LibraryMetadata metadata = LibraryMetadata.newBuilder().setVersion("1.0.0").build(); + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), metadata); + assertThat(actual).isNull(); + } + + @Test + void create_shouldReturnNull_whenArtifactNameIsEmpty() { + LibraryMetadata metadata = LibraryMetadata.newBuilder().setArtifactName("").build(); + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), metadata); + assertThat(actual).isNull(); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java deleted file mode 100644 index 9726101c07..0000000000 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.Answers.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.*; - -import io.opentelemetry.api.OpenTelemetry; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class GoldenSignalsMetricsTracerFactoryTest { - - private GoldenSignalsMetricsTracerFactory tracerFactory; - - @BeforeEach - void setUp() { - tracerFactory = new GoldenSignalsMetricsTracerFactory(OpenTelemetry.noop()); - } - - @Test - void newTracerWithSpanName_shouldCreateTracer_ifMetricsRecorderIsNotNull() { - tracerFactory.withContext(ApiTracerContext.empty()); - ApiTracer actual = - tracerFactory.newTracer( - mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); - assertThat(actual).isInstanceOf(GoldenSignalsMetricsTracer.class); - } - - @Test - void newTracerWithSpanName_shouldCreateBaseTracer_ifMetricsRecorderIsNull() { - ApiTracer actual = - tracerFactory.newTracer( - mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); - assertThat(actual).isInstanceOf(BaseApiTracer.class); - } - - @Test - void newTracerWithApiTracerContext_shouldMergeApiTracerContext() { - ApiTracerContext clientLevelTracerContext = mock(ApiTracerContext.class, RETURNS_DEEP_STUBS); - ApiTracerContext methodLevelTracerContext = mock(ApiTracerContext.class); - when(clientLevelTracerContext.libraryMetadata().artifactName()).thenReturn("does not matter"); - when(clientLevelTracerContext.merge(methodLevelTracerContext)) - .thenReturn(clientLevelTracerContext); - - tracerFactory.withContext(clientLevelTracerContext); - ApiTracer actual = tracerFactory.newTracer(mock(ApiTracer.class), methodLevelTracerContext); - - verify(clientLevelTracerContext).merge(methodLevelTracerContext); - assertThat(actual).isInstanceOf(GoldenSignalsMetricsTracer.class); - } - - @Test - void newTracerWithApiTracerContext_shouldCreateBaseTracer_ifMetricsRecorderIsNull() { - ApiTracer actual = tracerFactory.newTracer(mock(ApiTracer.class), mock(ApiTracerContext.class)); - - assertThat(actual).isInstanceOf(BaseApiTracer.class); - } -} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.java new file mode 100644 index 0000000000..379ab14206 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerFactoryTest.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 org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class LoggingTracerFactoryTest { + + @Test + void testNewTracer_CreatesLoggingTracer() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracer tracer = + factory.newTracer( + BaseApiTracer.getInstance(), + SpanName.of("client", "method"), + ApiTracerFactory.OperationType.Unary); + + assertNotNull(tracer); + assertTrue(tracer instanceof LoggingTracer); + } + + @Test + void testNewTracer_WithContext_CreatesLoggingTracer() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracer tracer = factory.newTracer(BaseApiTracer.getInstance(), ApiTracerContext.empty()); + + assertNotNull(tracer); + assertTrue(tracer instanceof LoggingTracer); + } + + @Test + void testWithContext_ReturnsNewFactoryWithMergedContext() { + LoggingTracerFactory factory = new LoggingTracerFactory(); + ApiTracerContext context = + ApiTracerContext.empty().toBuilder().setServerAddress("address").build(); + LoggingTracerFactory updatedFactory = (LoggingTracerFactory) factory.withContext(context); + + assertNotNull(updatedFactory); + assertTrue(updatedFactory instanceof LoggingTracerFactory); + assertEquals("address", updatedFactory.getApiTracerContext().serverAddress()); + } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + LoggingTracerFactory factoryWithoutContext = new LoggingTracerFactory(); + assertTrue(factoryWithoutContext.needsContext()); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + LoggingTracerFactory factoryWithoutContext = new LoggingTracerFactory(); + + ApiTracerContext context = + ApiTracerContext.empty().toBuilder().setServerAddress("address").build(); + LoggingTracerFactory factoryWithContext = + (LoggingTracerFactory) factoryWithoutContext.withContext(context); + + org.junit.jupiter.api.Assertions.assertFalse(factoryWithContext.needsContext()); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java new file mode 100644 index 0000000000..d1ba025d38 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/LoggingTracerTest.java @@ -0,0 +1,224 @@ +/* + * 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.junit.jupiter.api.Assertions.assertEquals; + +import com.google.api.gax.logging.TestLogger; +import com.google.api.gax.rpc.ApiExceptionFactory; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class LoggingTracerTest { + + private TestLogger testLogger; + + @BeforeEach + void setUp() { + testLogger = (TestLogger) LoggerFactory.getLogger(LoggingTracer.class); + testLogger.getMessageList().clear(); + testLogger.getMDCMap().clear(); + testLogger.getKeyValuePairsMap().clear(); + } + + @Test + void testAttemptFailedDuration_LogsError() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("generic failure duration"); + tracer.attemptFailedDuration(error, java.time.Duration.ZERO); + + assertEquals(1, testLogger.getMessageList().size()); + assertEquals("generic failure duration", testLogger.getMessageList().get(0)); + } + + @Test + void testAttemptFailedRetriesExhausted_LogsError() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("generic failure retries exhausted"); + tracer.attemptFailedRetriesExhausted(error); + + assertEquals(1, testLogger.getMessageList().size()); + assertEquals("generic failure retries exhausted", testLogger.getMessageList().get(0)); + } + + @Test + void testAttemptPermanentFailure_LogsError() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("generic permanent failure"); + tracer.attemptPermanentFailure(error); + + assertEquals(1, testLogger.getMessageList().size()); + assertEquals("generic permanent failure", testLogger.getMessageList().get(0)); + } + + @Test + void testRecordActionableError_logsErrorMessage() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("test error message"); + tracer.recordActionableError(error); + + assertEquals(1, testLogger.getMessageList().size()); + assertEquals("test error message", testLogger.getMessageList().get(0)); + } + + @Test + void testRecordActionableError_logsStatus() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = + ApiExceptionFactory.createException( + "test error message", + new RuntimeException("cause"), + FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT), + false); + + tracer.recordActionableError(error); + + Map attributesMap = getAttributesMap(); + assertEquals( + "INVALID_ARGUMENT", + attributesMap.get(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)); + } + + @Test + void testRecordActionableError_logsAttributes() { + ApiTracerContext context = + ApiTracerContext.empty().toBuilder().setServiceName("test-service").build(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("generic failure"); + tracer.recordActionableError(error); + + Map attributesMap = getAttributesMap(); + assertEquals( + "test-service", attributesMap.get(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)); + } + + @Test + void testRecordActionableError_logsErrorInfo() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + ErrorInfo errorInfo = + ErrorInfo.newBuilder() + .setReason("TEST_REASON") + .setDomain("test.domain.com") + .putMetadata("test_key", "test_value") + .build(); + + ErrorDetails errorDetails = + ErrorDetails.builder() + .setRawErrorMessages(Collections.singletonList(Any.pack(errorInfo))) + .build(); + + Exception error = + ApiExceptionFactory.createException( + "test error message", + new RuntimeException("cause"), + FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT), + false, + errorDetails); + + tracer.recordActionableError(error); + + Map attributesMap = getAttributesMap(); + assertEquals("TEST_REASON", attributesMap.get(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)); + assertEquals( + "test.domain.com", attributesMap.get(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE)); + assertEquals( + "test_value", + attributesMap.get(ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + "test_key")); + } + + @Test + void testRecordActionableError_logsExceptionDetails() { + ApiTracerContext context = ApiTracerContext.empty(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = new RuntimeException("test error message"); + tracer.recordActionableError(error); + + Map attributesMap = getAttributesMap(); + assertEquals( + "java.lang.RuntimeException", + attributesMap.get(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE)); + assertEquals( + "test error message", + attributesMap.get(ObservabilityAttributes.EXCEPTION_MESSAGE_ATTRIBUTE)); + } + + @Test + void testRecordActionableError_logsHttpStatus() { + ApiTracerContext context = + ApiTracerContext.empty().toBuilder().setTransport(ApiTracerContext.Transport.HTTP).build(); + LoggingTracer tracer = new LoggingTracer(context); + + Exception error = + ApiExceptionFactory.createException( + "test error message", + new RuntimeException("cause"), + FakeStatusCode.of(StatusCode.Code.INVALID_ARGUMENT), + false); + + tracer.recordActionableError(error); + + Map attributesMap = getAttributesMap(); + assertEquals( + "INVALID_ARGUMENT", + attributesMap.get(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)); + assertEquals(400L, attributesMap.get(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)); + } + + private Map getAttributesMap() { + if (!testLogger.getMDCMap().isEmpty()) { + return testLogger.getMDCMap(); + } else { + return testLogger.getKeyValuePairsMap(); + } + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java index 0af3be4746..d0da067067 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java @@ -47,23 +47,23 @@ void testExtractStatus_errorConversion_apiExceptions() { ApiException error = new ApiException( "fake_error", null, new FakeStatusCode(StatusCode.Code.INVALID_ARGUMENT), false); - String errorCode = ObservabilityUtils.extractStatus(error); - assertThat(errorCode).isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString()); + StatusCode.Code errorCode = ObservabilityUtils.extractStatus(error); + assertThat(errorCode).isEqualTo(StatusCode.Code.INVALID_ARGUMENT); } @Test void testExtractStatus_errorConversion_noError() { // test "OK", which corresponds to a "null" error. - String successCode = ObservabilityUtils.extractStatus(null); - assertThat(successCode).isEqualTo(StatusCode.Code.OK.toString()); + StatusCode.Code successCode = ObservabilityUtils.extractStatus(null); + assertThat(successCode).isEqualTo(StatusCode.Code.OK); } @Test void testExtractStatus_errorConversion_unknownException() { // test "UNKNOWN" Throwable unknownException = new RuntimeException(); - String errorCode2 = ObservabilityUtils.extractStatus(unknownException); - assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN.toString()); + StatusCode.Code errorCode2 = ObservabilityUtils.extractStatus(unknownException); + assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN); } @Test @@ -114,8 +114,147 @@ void testToOtelAttributes_shouldMapIntAttributes() { .isEqualTo((long) attribute2Value); } + @Test + void testGetResponseAttributes_grpc_success() { + Map attributes = + ObservabilityUtils.getResponseAttributes(null, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "OK"); + } + + @Test + void testGetResponseAttributes_grpc_apiException() { + ApiException error = + new ApiException("fake_error", null, new FakeStatusCode(StatusCode.Code.NOT_FOUND), false); + Map attributes = + ObservabilityUtils.getResponseAttributes(error, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "NOT_FOUND"); + } + + @Test + void testGetResponseAttributes_grpc_cancellationException() { + Throwable error = new java.util.concurrent.CancellationException(); + Map attributes = + ObservabilityUtils.getResponseAttributes(error, ApiTracerContext.Transport.GRPC); + assertThat(attributes) + .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "CANCELLED"); + } + + @Test + void testGetResponseAttributes_http_success() { + Map attributes = + ObservabilityUtils.getResponseAttributes(null, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.OK.getHttpStatusCode()); + } + + @Test + void testGetResponseAttributes_http_apiExceptionWithIntegerTransportCode() { + ApiException error = + new ApiException( + "fake_error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return StatusCode.Code.NOT_FOUND.getHttpStatusCode(); + } + }, + false); + Map attributes = + ObservabilityUtils.getResponseAttributes(error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.NOT_FOUND.getHttpStatusCode()); + } + + @Test + void testGetResponseAttributes_http_apiExceptionWithNonIntegerTransportCode() { + ApiException error = + new ApiException( + "fake_error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return "Not Found"; + } + }, + false); + Map attributes = + ObservabilityUtils.getResponseAttributes(error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.NOT_FOUND.getHttpStatusCode()); + } + + @Test + void testGetResponseAttributes_http_cancellationException() { + Throwable error = new java.util.concurrent.CancellationException(); + Map attributes = + ObservabilityUtils.getResponseAttributes(error, ApiTracerContext.Transport.HTTP); + assertThat(attributes) + .containsEntry( + ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, + (long) StatusCode.Code.CANCELLED.getHttpStatusCode()); + } + @Test void testToOtelAttributes_shouldReturnEmptyAttributes_nullInput() { assertThat(ObservabilityUtils.toOtelAttributes(null)).isEqualTo(Attributes.empty()); } + + @Test + void testSanitizeUrlFull_redactsUserInfo() { + String url = "https://user:password@example.com/some/path?foo=bar"; + String sanitized = ObservabilityUtils.sanitizeUrlFull(url); + assertThat(sanitized).isEqualTo("https://REDACTED:REDACTED@example.com/some/path?foo=bar"); + } + + @Test + void testSanitizeUrlFull_redactsSensitiveQueryParameters_caseInsensitive() { + String url = + "https://example.com/some/path?upload_Id=secret&AWSAccessKeyId=123&foo=bar&API_KEY=my_key"; + String sanitized = ObservabilityUtils.sanitizeUrlFull(url); + assertThat(sanitized) + .isEqualTo( + "https://example.com/some/path?upload_Id=REDACTED&AWSAccessKeyId=REDACTED&foo=bar&API_KEY=REDACTED"); + } + + @Test + void testSanitizeUrlFull_handlesKeyOnlyParameters() { + String url = "https://example.com/some/path?api_key&foo=bar"; + String sanitized = ObservabilityUtils.sanitizeUrlFull(url); + assertThat(sanitized).isEqualTo("https://example.com/some/path?api_key&foo=bar"); + } + + @Test + void testSanitizeUrlFull_handlesMalformedUrl() { + String url = "invalid::url:"; + String sanitized = ObservabilityUtils.sanitizeUrlFull(url); + // Unparsable URLs should be returned as empty string + assertThat(sanitized).isEmpty(); + } + + @Test + void testSanitizeUrlFull_noQueryOrUserInfo() { + String url = "https://example.com/some/path"; + String sanitized = ObservabilityUtils.sanitizeUrlFull(url); + assertThat(sanitized).isEqualTo(url); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactoryTest.java new file mode 100644 index 0000000000..e25b3d93f0 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsFactoryTest.java @@ -0,0 +1,147 @@ +/* + * 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.Answers.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.*; + +import com.google.api.gax.rpc.LibraryMetadata; +import io.opentelemetry.api.OpenTelemetry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OpenTelemetryMetricsFactoryTest { + + private OpenTelemetryMetricsFactory tracerFactory; + + @BeforeEach + void setUp() { + tracerFactory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + } + + @Test + void newTracerWithSpanName_shouldCreateTracer_ifMetricsRecorderIsNotNull() { + LibraryMetadata metadata = + LibraryMetadata.newBuilder().setArtifactName("gax-java").setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + tracerFactory.withContext(context); + ApiTracer actual = + tracerFactory.newTracer( + mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); + assertThat(actual).isInstanceOf(OpenTelemetryMetricsTracer.class); + } + + @Test + void newTracerWithSpanName_shouldCreateBaseTracer_ifMetricsRecorderIsNull() { + ApiTracer actual = + tracerFactory.newTracer( + mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); + assertThat(actual).isInstanceOf(BaseApiTracer.class); + } + + @Test + void newTracerWithApiTracerContext_shouldMergeApiTracerContext() { + ApiTracerContext clientLevelTracerContext = mock(ApiTracerContext.class, RETURNS_DEEP_STUBS); + ApiTracerContext methodLevelTracerContext = mock(ApiTracerContext.class); + when(clientLevelTracerContext.libraryMetadata().artifactName()).thenReturn("gax-java"); + when(clientLevelTracerContext.libraryMetadata().isEmpty()).thenReturn(false); + when(clientLevelTracerContext.merge(methodLevelTracerContext)) + .thenReturn(clientLevelTracerContext); + + tracerFactory.withContext(clientLevelTracerContext); + ApiTracer actual = tracerFactory.newTracer(mock(ApiTracer.class), methodLevelTracerContext); + + verify(clientLevelTracerContext).merge(methodLevelTracerContext); + assertThat(actual).isInstanceOf(OpenTelemetryMetricsTracer.class); + } + + @Test + void testWithContext_nullContext_returnsBaseApiTracerFactory() { + OpenTelemetryMetricsFactory factory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + ApiTracerFactory factoryWithContext = factory.withContext(null); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullMetadata_returnsBaseApiTracerFactory() { + OpenTelemetryMetricsFactory factory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + ApiTracerFactory factoryWithContext = factory.withContext(ApiTracerContext.empty()); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_emptyArtifactName_returnsBaseApiTracerFactory() { + OpenTelemetryMetricsFactory factory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + LibraryMetadata metadata = + LibraryMetadata.newBuilder().setArtifactName("").setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullArtifactName_returnsBaseApiTracerFactory() { + OpenTelemetryMetricsFactory factory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + LibraryMetadata metadata = LibraryMetadata.newBuilder().setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void newTracerWithApiTracerContext_shouldCreateBaseTracer_ifMetricsRecorderIsNull() { + OpenTelemetryMetricsFactory factory = new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + ApiTracer actual = factory.newTracer(mock(ApiTracer.class), mock(ApiTracerContext.class)); + + assertThat(actual).isInstanceOf(BaseApiTracer.class); + } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + OpenTelemetryMetricsFactory factoryWithoutContext = + new OpenTelemetryMetricsFactory(OpenTelemetry.noop()); + + assertThat(factoryWithoutContext.needsContext()).isTrue(); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + LibraryMetadata metadata = + LibraryMetadata.newBuilder().setArtifactName("gax-java").setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + tracerFactory.withContext(context); + + assertThat(tracerFactory.needsContext()).isFalse(); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracerTest.java similarity index 61% rename from gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java rename to gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracerTest.java index 1d70445a3e..965251084c 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsTracerTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.testing.FakeStatusCode; import com.google.common.testing.FakeTicker; @@ -47,14 +48,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -class GoldenSignalsMetricsTracerTest { +class OpenTelemetryMetricsTracerTest { private static final String ARTIFACT_NAME = "test-library"; public static final int TEST_REQUEST_DURATION_NANO = 2345698; public static final double EXPECTED_REQUEST_DURATION_SECOND = 2345698 / 1_000_000_000.0; private InMemoryMetricReader metricReader; - private GoldenSignalsMetricsTracer tracer; + private OpenTelemetryMetricsTracer tracer; private FakeTicker ticker; @@ -67,8 +68,13 @@ void setUp() { OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); ticker = new FakeTicker(); tracer = - new GoldenSignalsMetricsTracer( - new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME), + new OpenTelemetryMetricsTracer( + GoldenSignalsMetricsRecorder.create( + openTelemetry, + LibraryMetadata.newBuilder() + .setArtifactName(ARTIFACT_NAME) + .setVersion("1.2.3") + .build()), ApiTracerContext.empty(), ticker); } @@ -129,8 +135,12 @@ void operationCancelled_shouldRecordsOKStatus() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo( Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CancellationException", AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), - StatusCode.Code.CANCELLED.toString())); + StatusCode.Code.CANCELLED.toString(), + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + java.util.concurrent.CancellationException.class.getName())); } @Test @@ -163,7 +173,75 @@ void operationFailed_shouldRecordsOKStatus() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo( Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "INTERNAL", AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), - StatusCode.Code.INTERNAL.toString())); + StatusCode.Code.INTERNAL.toString(), + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + com.google.api.gax.rpc.ApiException.class.getName())); + } + + @Test + void operationFailed_shouldRecordCancellationException() { + java.util.concurrent.CancellationException error = + new java.util.concurrent.CancellationException("test cancellation"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CancellationException", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.CANCELLED.toString(), + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + java.util.concurrent.CancellationException.class.getName())); + } + + @Test + void operationFailed_shouldRecordClientTimeout() { + java.net.SocketTimeoutException error = new java.net.SocketTimeoutException("test timeout"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CLIENT_TIMEOUT", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.UNKNOWN.toString(), + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + java.net.SocketTimeoutException.class.getName())); + } + + @Test + void operationFailed_shouldRecordClientRequestError() { + IllegalArgumentException error = new IllegalArgumentException("test illegal argument"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CLIENT_REQUEST_ERROR", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.UNKNOWN.toString(), + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + java.lang.IllegalArgumentException.class.getName())); } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java similarity index 64% rename from gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java rename to gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java index 6054c67ca7..b7caed8178 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerFactoryTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerFactoryTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -41,6 +42,7 @@ import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.tracing.ApiTracerContext.Transport; import com.google.api.gax.tracing.ApiTracerFactory.OperationType; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -53,26 +55,37 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; -class SpanTracerFactoryTest { +class OpenTelemetryTracingFactoryTest { + private OpenTelemetry openTelemetry; private Tracer tracer; private SpanBuilder spanBuilder; private Span span; + private LibraryMetadata validMetadata; + @BeforeEach void setUp() { + openTelemetry = mock(OpenTelemetry.class); tracer = mock(Tracer.class); spanBuilder = mock(SpanBuilder.class); span = mock(Span.class); + when(openTelemetry.getTracer(nullable(String.class), nullable(String.class))) + .thenReturn(tracer); when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder); when(spanBuilder.setSpanKind(any())).thenReturn(spanBuilder); when(spanBuilder.setAllAttributes(any(Attributes.class))).thenReturn(spanBuilder); when(spanBuilder.startSpan()).thenReturn(span); + + validMetadata = mock(LibraryMetadata.class); + when(validMetadata.artifactName()).thenReturn("gax-java"); + when(validMetadata.version()).thenReturn("2.1.0"); } @ParameterizedTest @ValueSource(booleans = {false, true}) - void testNewTracer_createsSpanTracer(boolean useContext) { - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + void testNewTracer_createsOpenTelemetryTracingTracer(boolean useContext) { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance; if (useContext) { ApiTracerContext context = @@ -86,17 +99,18 @@ void testNewTracer_createsSpanTracer(boolean useContext) { tracerInstance = factory.newTracer(null, SpanName.of("service", "method"), OperationType.Unary); } - assertThat(tracerInstance).isInstanceOf(SpanTracer.class); + assertThat(tracerInstance).isInstanceOf(OpenTelemetryTracingTracer.class); } @ParameterizedTest @ValueSource(booleans = {false, true}) void testNewTracer_addsAttributes(boolean useContext) { - ApiTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + ApiTracerFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); factory = factory.withContext( ApiTracerContext.newBuilder() - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .setServerAddress("test-address") .build()); ApiTracer tracerInstance; @@ -105,7 +119,7 @@ void testNewTracer_addsAttributes(boolean useContext) { ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factory.newTracer(null, context); } else { @@ -128,11 +142,12 @@ void testNewTracer_addsAttributes(boolean useContext) { void testWithContext_addsInferredAttributes(boolean useContext) { ApiTracerContext context = ApiTracerContext.newBuilder() - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .setServerAddress("example.com") .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracerFactory factoryWithContext = factory.withContext(context); ApiTracer tracerInstance; @@ -141,7 +156,7 @@ void testWithContext_addsInferredAttributes(boolean useContext) { ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factoryWithContext.newTracer(null, callContext); } else { @@ -164,9 +179,11 @@ void testWithContext_addsInferredAttributes(boolean useContext) { @ParameterizedTest @ValueSource(booleans = {false, true}) void testWithContext_noEndpointContext_doesNotAddServerAddressAttribute(boolean useContext) { - ApiTracerContext context = ApiTracerContext.empty(); + ApiTracerContext context = + ApiTracerContext.newBuilder().setLibraryMetadata(validMetadata).build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracerFactory factoryWithContext = factory.withContext(context); ApiTracer tracerInstance; @@ -175,7 +192,7 @@ void testWithContext_noEndpointContext_doesNotAddServerAddressAttribute(boolean ApiTracerContext.newBuilder() .setFullMethodName("service/method") .setTransport(Transport.GRPC) - .setLibraryMetadata(LibraryMetadata.empty()) + .setLibraryMetadata(validMetadata) .build(); tracerInstance = factoryWithContext.newTracer(null, callContext); } else { @@ -203,7 +220,8 @@ void testNewTracer_withContext_grpc_usesFullMethodName() { .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -229,7 +247,8 @@ void testNewTracer_withContext_http_usesHttpMethodAndPathTemplate( .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -246,7 +265,8 @@ void testNewTracer_withContext_http_noHttpMethodOrPathTemplate_usesFullMethodNam .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, context); tracerInstance.attemptStarted(null, 1); @@ -256,7 +276,8 @@ void testNewTracer_withContext_http_noHttpMethodOrPathTemplate_usesFullMethodNam @Test void testNewTracer_withSpanName_usesPlaceholder() { - SpanTracerFactory factory = new SpanTracerFactory(tracer, ApiTracerContext.empty()); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); ApiTracer tracerInstance = factory.newTracer(null, SpanName.of("Service", "Method"), OperationType.Unary); @@ -272,7 +293,8 @@ void testNewTracer_mergesFactoryContext() { .setServerAddress("factory-address") .setLibraryMetadata(LibraryMetadata.empty()) .build(); - SpanTracerFactory factory = new SpanTracerFactory(tracer, apiTracerContext); + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, apiTracerContext); ApiTracerContext callContext = ApiTracerContext.newBuilder() @@ -293,4 +315,94 @@ void testNewTracer_mergesFactoryContext() { assertThat(attributes.asMap()) .containsEntry(AttributeKey.stringKey("rpc.method"), "Service/Method"); } + + @Test + void testNoOpWhenTracerNull() { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, null, ApiTracerContext.empty()); + + ApiTracer tracerInstance = + factory.newTracer(null, SpanName.of("Service", "Method"), OperationType.Unary); + + assertThat(tracerInstance).isInstanceOf(BaseApiTracer.class); + + ApiTracer tracerInstance2 = factory.newTracer(null, ApiTracerContext.empty()); + + assertThat(tracerInstance2).isInstanceOf(BaseApiTracer.class); + } + + @Test + void testWithContext_nullContext_returnsBaseApiTracerFactory() { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); + ApiTracerFactory factoryWithContext = factory.withContext(null); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullMetadata_returnsBaseApiTracerFactory() { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); + // Assuming ApiTracerContext.empty() has null libraryMetadata + ApiTracerFactory factoryWithContext = factory.withContext(ApiTracerContext.empty()); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_emptyArtifactName_returnsBaseApiTracerFactory() { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); + LibraryMetadata metadata = + LibraryMetadata.newBuilder().setArtifactName("").setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullArtifactName_returnsBaseApiTracerFactory() { + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); + LibraryMetadata metadata = LibraryMetadata.newBuilder().setVersion("1.0").build(); + ApiTracerContext context = ApiTracerContext.newBuilder().setLibraryMetadata(metadata).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testWithContext_nullTracer_returnsBaseApiTracerFactory() { + OpenTelemetry mockOpenTelemetry = mock(OpenTelemetry.class); + when(mockOpenTelemetry.getTracer(nullable(String.class), nullable(String.class))) + .thenReturn(null); + + OpenTelemetryTracingFactory factory = + new OpenTelemetryTracingFactory(mockOpenTelemetry, tracer, ApiTracerContext.empty()); + ApiTracerContext context = + ApiTracerContext.newBuilder().setLibraryMetadata(LibraryMetadata.empty()).build(); + + ApiTracerFactory factoryWithContext = factory.withContext(context); + assertThat(factoryWithContext).isInstanceOf(BaseApiTracerFactory.class); + } + + @Test + void testNeedsContext_returnsTrueWhenContextIsEmpty() { + OpenTelemetryTracingFactory factoryWithoutContext = + new OpenTelemetryTracingFactory(openTelemetry, tracer, ApiTracerContext.empty()); + assertThat(factoryWithoutContext.needsContext()).isTrue(); + } + + @Test + void testNeedsContext_returnsFalseWhenContextIsNotEmpty() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(validMetadata) + .setServerAddress("test-address") + .build(); + OpenTelemetryTracingFactory factoryWithContext = + new OpenTelemetryTracingFactory(openTelemetry, tracer, context); + + assertThat(factoryWithContext.needsContext()).isFalse(); + } } diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java new file mode 100644 index 0000000000..33fa2efcc0 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryTracingTracerTest.java @@ -0,0 +1,683 @@ +/* + * 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.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.api.gax.rpc.StatusCode; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +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.net.ConnectException; +import java.net.SocketTimeoutException; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class OpenTelemetryTracingTracerTest { + @Mock private Tracer tracer; + @Mock private SpanBuilder spanBuilder; + @Mock private Span span; + private OpenTelemetryTracingTracer openTelemetryTracingTracer; + private static final String ATTEMPT_SPAN_NAME = "Service/Method/attempt"; + + @BeforeEach + void setUp() { + when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder); + when(spanBuilder.setSpanKind(any(SpanKind.class))).thenReturn(spanBuilder); + when(spanBuilder.setAllAttributes(any(Attributes.class))).thenReturn(spanBuilder); + when(spanBuilder.startSpan()).thenReturn(span); + openTelemetryTracingTracer = + new OpenTelemetryTracingTracer(tracer, ApiTracerContext.empty(), ATTEMPT_SPAN_NAME); + } + + @Test + void testAttemptLifecycle_startsAndEndsAttemptSpan() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + openTelemetryTracingTracer.attemptSucceeded(); + + verify(tracer).spanBuilder(ATTEMPT_SPAN_NAME); + verify(spanBuilder).setSpanKind(SpanKind.CLIENT); + verify(span).end(); + } + + @Test + void testAttemptSucceeded_grpc() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + openTelemetryTracingTracer = new OpenTelemetryTracingTracer(tracer, context, ATTEMPT_SPAN_NAME); + + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + openTelemetryTracingTracer.attemptSucceeded(); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE), "OK"); + } + + @Test + void testAttemptSucceeded_http() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + openTelemetryTracingTracer = new OpenTelemetryTracingTracer(tracer, context, ATTEMPT_SPAN_NAME); + + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + openTelemetryTracingTracer.attemptSucceeded(); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE), 200L); + } + + @Test + void testResponseHeadersReceived_setsContentLengthAttribute() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", 12345L); + openTelemetryTracingTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 12345L); + } + + @Test + void testResponseHeadersReceived_variousContentLengthStringFormats() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map headers = new java.util.HashMap<>(); + headers.put("content-length", "6789"); + openTelemetryTracingTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 6789L); + } + + @Test + void testResponseHeadersReceived_missingContentLength() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map headers = new java.util.HashMap<>(); + headers.put("Other-Header", "123"); + openTelemetryTracingTracer.responseHeadersReceived(headers); + + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + void testResponseHeadersReceived_badFormat() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", "12X3"); + openTelemetryTracingTracer.responseHeadersReceived(headers); + + verify(span, org.mockito.Mockito.never()) + .setAttribute( + org.mockito.ArgumentMatchers.eq(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE), + org.mockito.ArgumentMatchers.anyLong()); + } + + @Test + void testResponseHeadersReceived_listContentLength() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map headers = new java.util.HashMap<>(); + headers.put("Content-Length", java.util.Arrays.asList(98765L)); + openTelemetryTracingTracer.responseHeadersReceived(headers); + + verify(span).setAttribute(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE, 98765L); + } + + void testAttemptStarted_noRetryAttributes_grpc() { + ApiTracerContext grpcContext = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + OpenTelemetryTracingTracer grpcTracer = + new OpenTelemetryTracingTracer(tracer, grpcContext, ATTEMPT_SPAN_NAME); + + // Initial attempt, attemptNumber is 0 + grpcTracer.attemptStarted(new Object(), 0); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); + assertThat(attributesCaptor.getValue().asMap()) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); + assertThat(attributesCaptor.getValue().asMap()) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); + } + + @Test + void testAttemptFailed_grpc() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + openTelemetryTracingTracer = new OpenTelemetryTracingTracer(tracer, context, ATTEMPT_SPAN_NAME); + + com.google.api.gax.rpc.ApiException exception = + new com.google.api.gax.rpc.ApiException( + "error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return null; + } + }, + false); + + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + openTelemetryTracingTracer.attemptFailedRetriesExhausted(exception); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE), + "NOT_FOUND"); + } + + @Test + void testAttemptStarted_retryAttributes_grpc() { + ApiTracerContext grpcContext = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.GRPC) + .build(); + OpenTelemetryTracingTracer grpcTracer = + new OpenTelemetryTracingTracer(tracer, grpcContext, ATTEMPT_SPAN_NAME); + + // N-th retry, attemptNumber is 5 + grpcTracer.attemptStarted(new Object(), 5); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); + Map, Object> capturedAttributes = attributesCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.longKey(ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE), 5L); + assertThat(capturedAttributes) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); + } + + @Test + void testAttemptFailed_http() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + openTelemetryTracingTracer = new OpenTelemetryTracingTracer(tracer, context, ATTEMPT_SPAN_NAME); + + com.google.api.gax.rpc.ApiException exception = + new com.google.api.gax.rpc.ApiException( + "error", + null, + new com.google.api.gax.rpc.StatusCode() { + @Override + public Code getCode() { + return Code.NOT_FOUND; + } + + @Override + public Object getTransportCode() { + return 404; + } + }, + false); + + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + openTelemetryTracingTracer.attemptFailedRetriesExhausted(exception); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + verify(span).end(); + + assertThat(attrsCaptor.getValue().asMap()) + .containsEntry( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE), 404L); + } + + @Test + void testAttemptStarted_noRetryAttributes_http() { + ApiTracerContext httpContext = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + OpenTelemetryTracingTracer httpTracer = + new OpenTelemetryTracingTracer(tracer, httpContext, ATTEMPT_SPAN_NAME); + + // Initial attempt, attemptNumber is 0 + httpTracer.attemptStarted(new Object(), 0); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); + Map, Object> capturedAttributes = attributesCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); + assertThat(capturedAttributes) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); + } + + @Test + void testAttemptStarted_retryAttributes_http() { + ApiTracerContext httpContext = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + OpenTelemetryTracingTracer httpTracer = + new OpenTelemetryTracingTracer(tracer, httpContext, ATTEMPT_SPAN_NAME); + + // N-th retry, attemptNumber is 5 + httpTracer.attemptStarted(new Object(), 5); + ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); + Map, Object> capturedAttributes = attributesCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .doesNotContainKey( + AttributeKey.longKey(ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.longKey(ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE), 5L); + } + + @Test + void testAttemptFailed_errorInfoReason() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + ErrorInfo errorInfo = ErrorInfo.newBuilder().setReason("RATE_LIMIT_EXCEEDED").build(); + ErrorDetails errorDetails = + ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(errorInfo))).build(); + Throwable cause = new Throwable("message"); + + ApiException apiException = + new ApiException( + cause, + new StatusCode() { + @Override + public Code getCode() { + return Code.UNAVAILABLE; + } + + @Override + public Object getTransportCode() { + return null; + } + }, + true, + errorDetails); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(apiException); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "RATE_LIMIT_EXCEEDED"); + + verify(span).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeGrpc() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + ApiException apiException = + new ApiException( + "message", + null, + new StatusCode() { + @Override + public Code getCode() { + return Code.PERMISSION_DENIED; + } + + @Override + public Object getTransportCode() { + return "PERMISSION_DENIED"; + } + }, + true); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(apiException); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "PERMISSION_DENIED"); + + verify(span).end(); + } + + @Test + void testAttemptFailed_specificServerErrorCodeHttp() { + ApiTracerContext context = + ApiTracerContext.newBuilder() + .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) + .setTransport(ApiTracerContext.Transport.HTTP) + .build(); + openTelemetryTracingTracer = new OpenTelemetryTracingTracer(tracer, context, ATTEMPT_SPAN_NAME); + + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + ApiException apiException = + new ApiException( + "message", + null, + new StatusCode() { + @Override + public Code getCode() { + return Code.PERMISSION_DENIED; + } + + @Override + public Object getTransportCode() { + return 403; + } + }, + true); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(apiException); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), "403"); + + verify(span).end(); + } + + @Test + void testAttemptFailed_clientTimeout() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(new SocketTimeoutException()); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + ErrorTypeUtil.ErrorType.CLIENT_TIMEOUT.toString()); + + verify(span).end(); + } + + @Test + void testAttemptFailed_clientConnectionError() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted( + new ConnectException("connection failed")); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + ErrorTypeUtil.ErrorType.CLIENT_CONNECTION_ERROR.toString()); + + verify(span).end(); + } + + @Test + void testAttemptFailed_clientRedirectError() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted( + new RedirectException("redirect failed")); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "RedirectException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientRequestError() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(new IllegalArgumentException()); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + ErrorTypeUtil.ErrorType.CLIENT_REQUEST_ERROR.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_clientUnknownError() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(new UnknownClientException()); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "UnknownClientException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_languageSpecificFallback() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted( + new IllegalStateException("illegal state")); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "IllegalStateException"); + verify(span).end(); + } + + @Test + void testAttemptFailed_internalFallback() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(new Throwable() {}); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, + // which triggers the + // fallback + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + ErrorTypeUtil.ErrorType.INTERNAL.toString()); + verify(span).end(); + } + + @Test + void testAttemptFailed_internalFallback_nullError() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted(null); + + // For an anonymous inner class Throwable, getSimpleName() is empty string, + // which triggers the + // fallback + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .doesNotContainKey(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)); + verify(span).end(); + } + + @Test + void testAttemptFailed_populatesExceptionTypeAndMessage() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + openTelemetryTracingTracer.attemptFailedRetriesExhausted( + new IllegalStateException("custom error message")); + + ArgumentCaptor attrsCaptor = ArgumentCaptor.forClass(Attributes.class); + verify(span).setAllAttributes(attrsCaptor.capture()); + Map, Object> capturedAttributes = attrsCaptor.getValue().asMap(); + assertThat(capturedAttributes) + .containsEntry( + AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE), + "java.lang.IllegalStateException"); + verify(span) + .setAttribute(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE, "custom error message"); + verify(span).end(); + } + + @Test + void testRequestUrlResolved_setsAttribute() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + String rawUrl = "https://example.com?api_key=secret"; + openTelemetryTracingTracer.requestUrlResolved(rawUrl); + + verify(span) + .setAttribute( + ObservabilityAttributes.HTTP_URL_FULL_ATTRIBUTE, + "https://example.com?api_key=REDACTED"); + } + + @Test + void testRequestUrlResolved_badUrl_notSet() { + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + String rawUrl = "htps:::://the-example"; + openTelemetryTracingTracer.requestUrlResolved(rawUrl); + + verify(span, never()) + .setAttribute(eq(ObservabilityAttributes.HTTP_URL_FULL_ATTRIBUTE), anyString()); + } + + private static class RedirectException extends RuntimeException { + public RedirectException(String message) { + super(message); + } + } + + private static class UnknownClientException extends RuntimeException {} + + @Test + void testInjectTraceContext_addsHeaders() { + io.opentelemetry.api.trace.SpanContext mockSpanContext = + io.opentelemetry.api.trace.SpanContext.create( + "00000000000000000000000000000001", + "0000000000000002", + io.opentelemetry.api.trace.TraceFlags.getSampled(), + io.opentelemetry.api.trace.TraceState.getDefault()); + io.opentelemetry.api.trace.Span realSpan = + io.opentelemetry.api.trace.Span.wrap(mockSpanContext); + when(spanBuilder.startSpan()).thenReturn(realSpan); + + openTelemetryTracingTracer = + new OpenTelemetryTracingTracer(tracer, ApiTracerContext.empty(), ATTEMPT_SPAN_NAME); + openTelemetryTracingTracer.attemptStarted(new Object(), 1); + + Map carrier = new java.util.HashMap<>(); + openTelemetryTracingTracer.injectTraceContext(carrier); + + assertThat(carrier).containsKey("traceparent"); + assertThat(carrier.get("traceparent")).contains("00000000000000000000000000000001"); + assertThat(carrier.get("traceparent")).contains("0000000000000002"); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java deleted file mode 100644 index 3e9fc53ce5..0000000000 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/SpanTracerTest.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * 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.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import io.opentelemetry.api.common.Attributes; -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 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 SpanTracerTest { - @Mock private Tracer tracer; - @Mock private SpanBuilder spanBuilder; - @Mock private Span span; - private SpanTracer spanTracer; - private static final String ATTEMPT_SPAN_NAME = "Service/Method/attempt"; - - @BeforeEach - void setUp() { - when(tracer.spanBuilder(anyString())).thenReturn(spanBuilder); - when(spanBuilder.setSpanKind(any(SpanKind.class))).thenReturn(spanBuilder); - when(spanBuilder.setAllAttributes(any(Attributes.class))).thenReturn(spanBuilder); - when(spanBuilder.startSpan()).thenReturn(span); - spanTracer = new SpanTracer(tracer, ApiTracerContext.empty(), ATTEMPT_SPAN_NAME); - } - - @Test - void testAttemptLifecycle_startsAndEndsAttemptSpan() { - spanTracer.attemptStarted(new Object(), 1); - spanTracer.attemptSucceeded(); - - verify(tracer).spanBuilder(ATTEMPT_SPAN_NAME); - verify(spanBuilder).setSpanKind(SpanKind.CLIENT); - verify(span).end(); - } - - @Test - void testAttemptStarted_includesLanguageAttribute() { - spanTracer.attemptStarted(new Object(), 1); - - ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); - verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); - - assertThat(attributesCaptor.getValue().asMap()) - .containsEntry( - io.opentelemetry.api.common.AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE), - SpanTracer.DEFAULT_LANGUAGE); - } - - @Test - void testAttemptStarted_noRetryAttributes_grpc() { - ApiTracerContext grpcContext = - ApiTracerContext.newBuilder() - .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) - .setTransport(ApiTracerContext.Transport.GRPC) - .build(); - SpanTracer grpcTracer = new SpanTracer(tracer, grpcContext, ATTEMPT_SPAN_NAME); - - // Initial attempt, attemptNumber is 0 - grpcTracer.attemptStarted(new Object(), 0); - ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); - verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); - assertThat(attributesCaptor.getValue().asMap()) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); - assertThat(attributesCaptor.getValue().asMap()) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); - } - - @Test - void testAttemptStarted_retryAttributes_grpc() { - ApiTracerContext grpcContext = - ApiTracerContext.newBuilder() - .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) - .setTransport(ApiTracerContext.Transport.GRPC) - .build(); - SpanTracer grpcTracer = new SpanTracer(tracer, grpcContext, ATTEMPT_SPAN_NAME); - - // N-th retry, attemptNumber is 5 - grpcTracer.attemptStarted(new Object(), 5); - ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); - verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); - java.util.Map, Object> capturedAttributes = - attributesCaptor.getValue().asMap(); - assertThat(capturedAttributes) - .containsEntry( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE), - 5L); - assertThat(capturedAttributes) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); - } - - @Test - void testAttemptStarted_noRetryAttributes_http() { - ApiTracerContext httpContext = - ApiTracerContext.newBuilder() - .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) - .setTransport(ApiTracerContext.Transport.HTTP) - .build(); - SpanTracer httpTracer = new SpanTracer(tracer, httpContext, ATTEMPT_SPAN_NAME); - - // Initial attempt, attemptNumber is 0 - httpTracer.attemptStarted(new Object(), 0); - ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); - verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); - java.util.Map, Object> capturedAttributes = - attributesCaptor.getValue().asMap(); - assertThat(capturedAttributes) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); - assertThat(capturedAttributes) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE)); - } - - @Test - void testAttemptStarted_retryAttributes_http() { - ApiTracerContext httpContext = - ApiTracerContext.newBuilder() - .setLibraryMetadata(com.google.api.gax.rpc.LibraryMetadata.empty()) - .setTransport(ApiTracerContext.Transport.HTTP) - .build(); - SpanTracer httpTracer = new SpanTracer(tracer, httpContext, ATTEMPT_SPAN_NAME); - - // N-th retry, attemptNumber is 5 - httpTracer.attemptStarted(new Object(), 5); - ArgumentCaptor attributesCaptor = ArgumentCaptor.forClass(Attributes.class); - verify(spanBuilder).setAllAttributes(attributesCaptor.capture()); - java.util.Map, Object> capturedAttributes = - attributesCaptor.getValue().asMap(); - assertThat(capturedAttributes) - .doesNotContainKey( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.GRPC_RESEND_COUNT_ATTRIBUTE)); - assertThat(capturedAttributes) - .containsEntry( - io.opentelemetry.api.common.AttributeKey.longKey( - ObservabilityAttributes.HTTP_RESEND_COUNT_ATTRIBUTE), - 5L); - } -} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracedUnaryCallableTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracedUnaryCallableTest.java index 15e637ebff..cedaf9a0d3 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracedUnaryCallableTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/TracedUnaryCallableTest.java @@ -208,30 +208,4 @@ void testResourceNameExtractorUsed() { assertThat(contextCaptor.getValue().destinationResourceId()) .isEqualTo("extracted-resource-name"); } - - @Test - void testExtractResourceNameToApiTracerContext_nullExtractor() { - tracedUnaryCallable = - new TracedUnaryCallable<>(innerCallable, tracerFactory, TRACER_CONTEXT, null); - ApiTracerContext context = tracedUnaryCallable.extractResourceNameToApiTracerContext("request"); - assertThat(context).isEqualTo(TRACER_CONTEXT); - } - - @Test - void testExtractResourceNameToApiTracerContext_extractorReturnsNull() { - tracedUnaryCallable = - new TracedUnaryCallable<>(innerCallable, tracerFactory, TRACER_CONTEXT, request -> null); - ApiTracerContext context = tracedUnaryCallable.extractResourceNameToApiTracerContext("request"); - assertThat(context).isEqualTo(TRACER_CONTEXT); - } - - @Test - void testExtractResourceNameToApiTracerContext_extractorReturnsResourceId() { - tracedUnaryCallable = - new TracedUnaryCallable<>( - innerCallable, tracerFactory, TRACER_CONTEXT, request -> "extracted-id"); - ApiTracerContext context = tracedUnaryCallable.extractResourceNameToApiTracerContext("request"); - assertThat(context).isNotSameInstanceAs(TRACER_CONTEXT); - assertThat(context.destinationResourceId()).isEqualTo("extracted-id"); - } } diff --git a/gax-java/pom.xml b/gax-java/pom.xml index a303096dc4..d75e45f30a 100644 --- a/gax-java/pom.xml +++ b/gax-java/pom.xml @@ -54,10 +54,23 @@ com.google.auth - google-auth-library-bom - ${google.auth.version} - pom - import + google-auth-library-credentials + 1.43.0 + + + com.google.auth + google-auth-library-oauth2-http + 1.43.0 + + + com.google.auth + google-auth-library-appengine + 1.43.0 + + + com.google.auth + google-auth-library-cab-token-generator + 1.43.0 org.threeten @@ -98,12 +111,12 @@ com.google.api gax - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT com.google.api gax - 2.76.1-SNAPSHOT + 2.76.1-SNAPSHOT test-jar testlib diff --git a/hermetic_build/DEVELOPMENT.md b/hermetic_build/DEVELOPMENT.md index 9d0eb5f6ce..ce1cd960e5 100644 --- a/hermetic_build/DEVELOPMENT.md +++ b/hermetic_build/DEVELOPMENT.md @@ -190,15 +190,21 @@ python hermetic_build/library_generation/cli/entry_point.py generate \ 2. Set the version of gapic-generator-java ```shell - LOCAL_GENERATOR_VERSION=$(mvn \ + export LOCAL_GENERATOR_VERSION=$(mvn \ org.apache.maven.plugins:maven-help-plugin:evaluate \ -Dexpression=project.version \ - -pl gapic-generator-java \ + -pl sdk-platform-java/gapic-generator-java \ -DforceStdout \ -q) ``` -3. Run the image +3. Clone the googleapis repository (API definitions) + ```shell + cd google-cloud-java + git clone https://github.com/googleapis/googleapis + ``` + +4. Run the image ```shell # Assume you want to generate the library in the current working directory @@ -208,13 +214,13 @@ python hermetic_build/library_generation/cli/entry_point.py generate \ --quiet \ -u "$(id -u):$(id -g)" \ -v "$(pwd):/workspace" \ - -v /path/to/api-definitions:/workspace/apis \ + -v "$(pwd)/googleapis:/googleapis" \ -e GENERATOR_VERSION="${LOCAL_GENERATOR_VERSION}" \ local:image-tag \ - --generation-config-path=/workspace/generation_config_file \ - --library-names=apigee-connect,asset \ + --generation-config-path=/workspace/generation_config.yaml \ + --library-names=translate \ --repository-path=/workspace \ - --api-definitions-path=/workspace/apis + --api-definitions-path=/googleapis ``` # Debugging tips diff --git a/hermetic_build/common/cli/get_changed_libraries.py b/hermetic_build/common/cli/get_changed_libraries.py index 0cc8a6cb34..4f2bfe45c6 100644 --- a/hermetic_build/common/cli/get_changed_libraries.py +++ b/hermetic_build/common/cli/get_changed_libraries.py @@ -21,6 +21,7 @@ import os import click +from common.model.config_change import ConfigChange from common.model.generation_config import GenerationConfig from common.utils.generation_config_comparator import compare_config @@ -51,9 +52,18 @@ def main(ctx): metadata about library generation. """, ) +@click.option( + "--force-regenerate-all", + required=False, + type=bool, + help=""" + Force regenerate all libraries. + """, +) def create( baseline_generation_config_path: str, current_generation_config_path: str, + force_regenerate_all: bool, ) -> None: """ Compares baseline generation config with current generation config and @@ -77,6 +87,7 @@ def create( config_change = compare_config( baseline_config=GenerationConfig.from_yaml(baseline_generation_config_path), current_config=GenerationConfig.from_yaml(current_generation_config_path), + force_regenerate_all=force_regenerate_all, ) click.echo(",".join(config_change.get_changed_libraries())) diff --git a/hermetic_build/common/utils/generation_config_comparator.py b/hermetic_build/common/utils/generation_config_comparator.py index f41299ddc2..8274af2074 100644 --- a/hermetic_build/common/utils/generation_config_comparator.py +++ b/hermetic_build/common/utils/generation_config_comparator.py @@ -25,7 +25,9 @@ def compare_config( - baseline_config: GenerationConfig, current_config: GenerationConfig + baseline_config: GenerationConfig, + current_config: GenerationConfig, + force_regenerate_all: bool = False, ) -> ConfigChange: """ Compare two GenerationConfig object and output a mapping from ConfigChange @@ -48,6 +50,12 @@ def compare_config( current_params = __convert_params_to_sorted_list( obj=current_config, excluded_params=excluded_params ) + if force_regenerate_all: + config_change = LibraryChange( + changed_param="force_regenerate_all", + current_value="true", + ) + diff[ChangeType.REPO_LEVEL_CHANGE].append(config_change) for baseline_param, current_param in zip(baseline_params, current_params): if baseline_param == current_param: @@ -60,7 +68,6 @@ def compare_config( current_value=current_param[1], ) diff[ChangeType.REPO_LEVEL_CHANGE].append(config_change) - __compare_libraries( diff=diff, baseline_library_configs=baseline_config.libraries, diff --git a/hermetic_build/library_generation/owlbot/src/fix_poms.py b/hermetic_build/library_generation/owlbot/src/fix_poms.py index 9ee7514a4d..f3e1882c65 100644 --- a/hermetic_build/library_generation/owlbot/src/fix_poms.py +++ b/hermetic_build/library_generation/owlbot/src/fix_poms.py @@ -273,7 +273,8 @@ def update_parent_pom(filename: str, modules: List[module.Module]): new_dependency.append(new_group) new_dependency.append(new_artifact) new_dependency.append(new_version) - new_dependency.append(comment) + if "vertexai" not in m.artifact_id: + new_dependency.append(comment) new_dependency.tail = "\n " dependencies.insert(1, new_dependency) @@ -310,7 +311,8 @@ def update_bom_pom(filename: str, modules: List[module.Module]): new_dependency.append(new_group) new_dependency.append(new_artifact) new_dependency.append(new_version) - new_dependency.append(comment) + if "vertexai" not in m.artifact_id: + new_dependency.append(comment) if index == num_modules - 1: new_dependency.tail = "\n " diff --git a/hermetic_build/library_generation/tests/owlbot/fix_poms_unit_tests.py b/hermetic_build/library_generation/tests/owlbot/fix_poms_unit_tests.py index 2e66454827..a284d837ec 100644 --- a/hermetic_build/library_generation/tests/owlbot/fix_poms_unit_tests.py +++ b/hermetic_build/library_generation/tests/owlbot/fix_poms_unit_tests.py @@ -35,6 +35,53 @@ def test_update_poms_group_id_does_not_start_with_google_correctly(self): for sub_dir in sub_dirs: self.__remove_file_in_subdir(ad_manager_resource, sub_dir) + def test_update_bom_pom_excludes_vertexai_comment(self): + import tempfile + from library_generation.owlbot.src.poms.module import Module + from library_generation.owlbot.src.fix_poms import update_bom_pom + + # Minimal XML structure + initial_xml = """ + + + + + +""" + + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as tmp: + tmp.write(initial_xml) + tmp_path = tmp.name + + try: + modules = [ + Module( + group_id="com.google.cloud", + artifact_id="google-cloud-vertexai", + version="1.0.0", + release_version="1.0.0", + ), + Module( + group_id="com.google.cloud", + artifact_id="google-cloud-datastore", + version="1.0.0", + release_version="1.0.0", + ), + ] + + update_bom_pom(tmp_path, modules) + + with open(tmp_path, "r") as f: + content = f.read() + + self.assertNotIn("x-version-update:google-cloud-vertexai:current", content) + self.assertIn("x-version-update:google-cloud-datastore:current", content) + + finally: + import os + + os.unlink(tmp_path) + @classmethod def __copy__golden(cls, base_dir: str, subdir: str): golden = os.path.join(base_dir, subdir, "pom-golden.xml") diff --git a/java-shared-dependencies/README.md b/java-shared-dependencies/README.md index 47abde1753..383d54d7d8 100644 --- a/java-shared-dependencies/README.md +++ b/java-shared-dependencies/README.md @@ -14,7 +14,7 @@ If you are using Maven, add this to the `dependencyManagement` section. com.google.cloud google-cloud-shared-dependencies - 3.58.0 + 3.59.0 pom import diff --git a/java-shared-dependencies/third-party-dependencies/pom.xml b/java-shared-dependencies/third-party-dependencies/pom.xml index 5a9e186834..049ada38cc 100644 --- a/java-shared-dependencies/third-party-dependencies/pom.xml +++ b/java-shared-dependencies/third-party-dependencies/pom.xml @@ -24,10 +24,10 @@ ${project.artifactId} 1.8.0 - 1.24 + 1.26 0.31.1 3.0.2 - 2.18.2 + 2.18.3 1.18.0 4.4.16 4.5.14 diff --git a/java-showcase/gapic-showcase/pom.xml b/java-showcase/gapic-showcase/pom.xml index bc8d2cc812..644bd919d3 100644 --- a/java-showcase/gapic-showcase/pom.xml +++ b/java-showcase/gapic-showcase/pom.xml @@ -17,7 +17,7 @@ - 0.36.2 + 0.39.0 1.2.13 1.5.25 @@ -233,6 +233,24 @@ + + org.slf4j + slf4j-api + 2.0.16 + test + + + ch.qos.logback + logback-classic + ${slf4j2-logback.version} + test + + + ch.qos.logback + logback-core + ${slf4j2-logback.version} + test + @@ -318,6 +336,7 @@ **/com/google/showcase/v1beta1/it/*.java **/com/google/showcase/v1beta1/it/logging/ITLoggingDisabled.java **/com/google/showcase/v1beta1/it/logging/ITLogging.java + **/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java @@ -363,6 +382,7 @@ **/com/google/showcase/v1beta1/it/*.java **/com/google/showcase/v1beta1/it/logging/ITLogging1x.java **/com/google/showcase/v1beta1/it/logging/ITLogging.java + **/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java diff --git a/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/SequenceServiceClient.java b/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/SequenceServiceClient.java index 29be19656f..bc5d665363 100644 --- a/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/SequenceServiceClient.java +++ b/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/SequenceServiceClient.java @@ -46,8 +46,11 @@ // AUTO-GENERATED DOCUMENTATION AND CLASS. /** - * This class provides the ability to make remote calls to the backing service through method calls - * that map to API methods. Sample code to get started: + * Service Description: A service that enables testing of unary and server streaming calls by + * specifying a specific, predictable sequence of responses from the service + * + *

This class provides the ability to make remote calls to the backing service through method + * calls that map to API methods. Sample code to get started: * *

{@code
  * // This snippet has been automatically generated and should be regarded as a code template only.
@@ -74,7 +77,7 @@
  *    
  *    
  *      

CreateSequence - *

Creates a sequence. + *

Create a sequence of responses to be returned as unary calls * *

Request object method variants only take one parameter, a request object, which must be constructed before the call.

*
    @@ -92,7 +95,7 @@ * * *

    CreateStreamingSequence - *

    Creates a sequence. + *

    Creates a sequence of responses to be returned in a server streaming call * *

    Request object method variants only take one parameter, a request object, which must be constructed before the call.

    *
      @@ -110,7 +113,7 @@ * * *

      GetSequenceReport - *

      Retrieves a sequence. + *

      Retrieves a sequence report which can be used to retrieve information about a sequence of unary calls. * *

      Request object method variants only take one parameter, a request object, which must be constructed before the call.

      *
        @@ -129,7 +132,7 @@ * * *

        GetStreamingSequenceReport - *

        Retrieves a sequence. + *

        Retrieves a sequence report which can be used to retrieve information about a sequences of responses in a server streaming call. * *

        Request object method variants only take one parameter, a request object, which must be constructed before the call.

        *
          @@ -148,7 +151,7 @@ * * *

          AttemptSequence - *

          Attempts a sequence. + *

          Attempts a sequence of unary responses. * *

          Request object method variants only take one parameter, a request object, which must be constructed before the call.

          *
            @@ -167,7 +170,7 @@ * * *

            AttemptStreamingSequence - *

            Attempts a streaming sequence. May not function as expected in HTTP mode due to when http statuses are sent See https://github.com/googleapis/gapic-showcase/issues/1377 for more details + *

            Attempts a server streaming call with a sequence of responses Can be used to test retries and stream resumption logic May not function as expected in HTTP mode due to when http statuses are sent See https://github.com/googleapis/gapic-showcase/issues/1377 for more details * *

            Callable method variants take no parameters and return an immutable API callable object, which can be used to initiate calls to the service.

            *
              @@ -359,7 +362,7 @@ public SequenceServiceStub getStub() { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Create a sequence of responses to be returned as unary calls * *

              Sample code: * @@ -386,7 +389,7 @@ public final Sequence createSequence(Sequence sequence) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Create a sequence of responses to be returned as unary calls * *

              Sample code: * @@ -412,7 +415,7 @@ public final Sequence createSequence(CreateSequenceRequest request) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Create a sequence of responses to be returned as unary calls * *

              Sample code: * @@ -438,7 +441,7 @@ public final UnaryCallable createSequenceCallab // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Creates a sequence of responses to be returned in a server streaming call * *

              Sample code: * @@ -465,7 +468,7 @@ public final StreamingSequence createStreamingSequence(StreamingSequence streami // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Creates a sequence of responses to be returned in a server streaming call * *

              Sample code: * @@ -493,7 +496,7 @@ public final StreamingSequence createStreamingSequence(CreateStreamingSequenceRe // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Creates a sequence. + * Creates a sequence of responses to be returned in a server streaming call * *

              Sample code: * @@ -522,7 +525,8 @@ public final StreamingSequence createStreamingSequence(CreateStreamingSequenceRe // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequence of unary + * calls. * *

              Sample code: * @@ -551,7 +555,8 @@ public final SequenceReport getSequenceReport(SequenceReportName name) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequence of unary + * calls. * *

              Sample code: * @@ -577,7 +582,8 @@ public final SequenceReport getSequenceReport(String name) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequence of unary + * calls. * *

              Sample code: * @@ -605,7 +611,8 @@ public final SequenceReport getSequenceReport(GetSequenceReportRequest request) // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequence of unary + * calls. * *

              Sample code: * @@ -633,7 +640,8 @@ public final UnaryCallable getSequence // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequences of + * responses in a server streaming call. * *

              Sample code: * @@ -663,7 +671,8 @@ public final StreamingSequenceReport getStreamingSequenceReport( // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequences of + * responses in a server streaming call. * *

              Sample code: * @@ -690,7 +699,8 @@ public final StreamingSequenceReport getStreamingSequenceReport(String name) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequences of + * responses in a server streaming call. * *

              Sample code: * @@ -719,7 +729,8 @@ public final StreamingSequenceReport getStreamingSequenceReport( // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Retrieves a sequence. + * Retrieves a sequence report which can be used to retrieve information about a sequences of + * responses in a server streaming call. * *

              Sample code: * @@ -748,7 +759,7 @@ public final StreamingSequenceReport getStreamingSequenceReport( // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Attempts a sequence. + * Attempts a sequence of unary responses. * *

              Sample code: * @@ -775,7 +786,7 @@ public final void attemptSequence(SequenceName name) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Attempts a sequence. + * Attempts a sequence of unary responses. * *

              Sample code: * @@ -801,7 +812,7 @@ public final void attemptSequence(String name) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Attempts a sequence. + * Attempts a sequence of unary responses. * *

              Sample code: * @@ -829,7 +840,7 @@ public final void attemptSequence(AttemptSequenceRequest request) { // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Attempts a sequence. + * Attempts a sequence of unary responses. * *

              Sample code: * @@ -856,8 +867,9 @@ public final UnaryCallable attemptSequenceCallabl // AUTO-GENERATED DOCUMENTATION AND METHOD. /** - * Attempts a streaming sequence. May not function as expected in HTTP mode due to when http - * statuses are sent See https://github.com/googleapis/gapic-showcase/issues/1377 for more details + * Attempts a server streaming call with a sequence of responses Can be used to test retries and + * stream resumption logic May not function as expected in HTTP mode due to when http statuses are + * sent See https://github.com/googleapis/gapic-showcase/issues/1377 for more details * *

              Sample code: * diff --git a/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/package-info.java b/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/package-info.java index dff2b42214..fa1a9c2465 100644 --- a/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/package-info.java +++ b/java-showcase/gapic-showcase/src/main/java/com/google/showcase/v1beta1/package-info.java @@ -125,6 +125,9 @@ * *

              ======================= SequenceServiceClient ======================= * + *

              Service Description: A service that enables testing of unary and server streaming calls by + * specifying a specific, predictable sequence of responses from the service + * *

              Sample for SequenceServiceClient: * *

              {@code
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/ComplianceClientHttpJsonTest.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/ComplianceClientHttpJsonTest.java
              index 39767b4ae8..d0b50ab4df 100644
              --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/ComplianceClientHttpJsonTest.java
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/ComplianceClientHttpJsonTest.java
              @@ -330,6 +330,16 @@ public void repeatDataSimplePathTest() throws Exception {
                                   .setFChild(ComplianceDataChild.newBuilder().build())
                                   .setPString("pString-1191954271")
                                   .setPInt32(-858673665)
              +                    .setPSint32(-567522134)
              +                    .setPSfixed32(1566619631)
              +                    .setPUint32(-510263832)
              +                    .setPFixed32(942872580)
              +                    .setPInt64(-858673570)
              +                    .setPSint64(-567522039)
              +                    .setPSfixed64(1566619726)
              +                    .setPUint64(-510263737)
              +                    .setPFixed64(942872675)
              +                    .setPFloat(-861507123)
                                   .setPDouble(-991225216)
                                   .setPBool(true)
                                   .setPChild(ComplianceDataChild.newBuilder().build())
              @@ -393,6 +403,16 @@ public void repeatDataSimplePathExceptionTest() throws Exception {
                                     .setFChild(ComplianceDataChild.newBuilder().build())
                                     .setPString("pString-1191954271")
                                     .setPInt32(-858673665)
              +                      .setPSint32(-567522134)
              +                      .setPSfixed32(1566619631)
              +                      .setPUint32(-510263832)
              +                      .setPFixed32(942872580)
              +                      .setPInt64(-858673570)
              +                      .setPSint64(-567522039)
              +                      .setPSfixed64(1566619726)
              +                      .setPUint64(-510263737)
              +                      .setPFixed64(942872675)
              +                      .setPFloat(-861507123)
                                     .setPDouble(-991225216)
                                     .setPBool(true)
                                     .setPChild(ComplianceDataChild.newBuilder().build())
              @@ -459,6 +479,16 @@ public void repeatDataPathResourceTest() throws Exception {
                                           .build())
                                   .setPString("pString-1191954271")
                                   .setPInt32(-858673665)
              +                    .setPSint32(-567522134)
              +                    .setPSfixed32(1566619631)
              +                    .setPUint32(-510263832)
              +                    .setPFixed32(942872580)
              +                    .setPInt64(-858673570)
              +                    .setPSint64(-567522039)
              +                    .setPSfixed64(1566619726)
              +                    .setPUint64(-510263737)
              +                    .setPFixed64(942872675)
              +                    .setPFloat(-861507123)
                                   .setPDouble(-991225216)
                                   .setPBool(true)
                                   .setPChild(ComplianceDataChild.newBuilder().build())
              @@ -536,6 +566,16 @@ public void repeatDataPathResourceExceptionTest() throws Exception {
                                             .build())
                                     .setPString("pString-1191954271")
                                     .setPInt32(-858673665)
              +                      .setPSint32(-567522134)
              +                      .setPSfixed32(1566619631)
              +                      .setPUint32(-510263832)
              +                      .setPFixed32(942872580)
              +                      .setPInt64(-858673570)
              +                      .setPSint64(-567522039)
              +                      .setPSfixed64(1566619726)
              +                      .setPUint64(-510263737)
              +                      .setPFixed64(942872675)
              +                      .setPFloat(-861507123)
                                     .setPDouble(-991225216)
                                     .setPBool(true)
                                     .setPChild(ComplianceDataChild.newBuilder().build())
              @@ -602,6 +642,16 @@ public void repeatDataPathTrailingResourceTest() throws Exception {
                                           .build())
                                   .setPString("pString-1191954271")
                                   .setPInt32(-858673665)
              +                    .setPSint32(-567522134)
              +                    .setPSfixed32(1566619631)
              +                    .setPUint32(-510263832)
              +                    .setPFixed32(942872580)
              +                    .setPInt64(-858673570)
              +                    .setPSint64(-567522039)
              +                    .setPSfixed64(1566619726)
              +                    .setPUint64(-510263737)
              +                    .setPFixed64(942872675)
              +                    .setPFloat(-861507123)
                                   .setPDouble(-991225216)
                                   .setPBool(true)
                                   .setPChild(ComplianceDataChild.newBuilder().build())
              @@ -679,6 +729,16 @@ public void repeatDataPathTrailingResourceExceptionTest() throws Exception {
                                             .build())
                                     .setPString("pString-1191954271")
                                     .setPInt32(-858673665)
              +                      .setPSint32(-567522134)
              +                      .setPSfixed32(1566619631)
              +                      .setPUint32(-510263832)
              +                      .setPFixed32(942872580)
              +                      .setPInt64(-858673570)
              +                      .setPSint64(-567522039)
              +                      .setPSfixed64(1566619726)
              +                      .setPUint64(-510263737)
              +                      .setPFixed64(942872675)
              +                      .setPFloat(-861507123)
                                     .setPDouble(-991225216)
                                     .setPBool(true)
                                     .setPChild(ComplianceDataChild.newBuilder().build())
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java
              new file mode 100644
              index 0000000000..8f386f03f2
              --- /dev/null
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITCompositeTracer.java
              @@ -0,0 +1,149 @@
              +/*
              + * 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.CompositeTracerFactory;
              +import com.google.api.gax.tracing.ObservabilityAttributes;
              +import com.google.api.gax.tracing.OpenTelemetryMetricsFactory;
              +import com.google.api.gax.tracing.OpenTelemetryTracingFactory;
              +import com.google.showcase.v1beta1.EchoClient;
              +import com.google.showcase.v1beta1.EchoRequest;
              +import com.google.showcase.v1beta1.it.util.TestClientInitializer;
              +import io.opentelemetry.api.GlobalOpenTelemetry;
              +import io.opentelemetry.api.common.AttributeKey;
              +import io.opentelemetry.sdk.OpenTelemetrySdk;
              +import io.opentelemetry.sdk.metrics.SdkMeterProvider;
              +import io.opentelemetry.sdk.metrics.data.MetricData;
              +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
              +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.Arrays;
              +import java.util.Collection;
              +import java.util.List;
              +import org.junit.jupiter.api.AfterEach;
              +import org.junit.jupiter.api.BeforeEach;
              +import org.junit.jupiter.api.Test;
              +
              +class ITCompositeTracer {
              +  private static final String SHOWCASE_SERVER_ADDRESS = "localhost";
              +  private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase";
              +
              +  private InMemorySpanExporter spanExporter;
              +  private InMemoryMetricReader metricReader;
              +  private OpenTelemetrySdk openTelemetrySdk;
              +
              +  @BeforeEach
              +  void setup() {
              +    spanExporter = InMemorySpanExporter.create();
              +    SdkTracerProvider tracerProvider =
              +        SdkTracerProvider.builder()
              +            .addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
              +            .build();
              +
              +    metricReader = InMemoryMetricReader.create();
              +    SdkMeterProvider meterProvider =
              +        SdkMeterProvider.builder().registerMetricReader(metricReader).build();
              +
              +    openTelemetrySdk =
              +        OpenTelemetrySdk.builder()
              +            .setTracerProvider(tracerProvider)
              +            .setMeterProvider(meterProvider)
              +            .buildAndRegisterGlobal();
              +  }
              +
              +  @AfterEach
              +  void tearDown() {
              +    if (openTelemetrySdk != null) {
              +      openTelemetrySdk.close();
              +    }
              +    GlobalOpenTelemetry.resetForTest();
              +  }
              +
              +  private CompositeTracerFactory createCompositeTracerFactory() {
              +    OpenTelemetryTracingFactory openTelemetryTracingFactory =
              +        new OpenTelemetryTracingFactory(openTelemetrySdk);
              +    OpenTelemetryMetricsFactory metricsTracerFactory =
              +        new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    return new CompositeTracerFactory(
              +        Arrays.asList(openTelemetryTracingFactory, metricsTracerFactory));
              +  }
              +
              +  @Test
              +  void testCompositeTracer() throws Exception {
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(createCompositeTracerFactory())) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("composite-tracing-test").build());
              +
              +      // Verify Span name and one basic attribute server.address
              +      List actualSpans = spanExporter.getFinishedSpanItems();
              +      assertThat(actualSpans).isNotEmpty();
              +
              +      SpanData attemptSpan =
              +          actualSpans.stream()
              +              .filter(span -> span.getName().equals("google.showcase.v1beta1.Echo/Echo"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Incorrect span name"));
              +      assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT);
              +      assertThat(
              +              attemptSpan
              +                  .getAttributes()
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_ADDRESS);
              +
              +      // Verify metric name and one basic attribute server.address
              +      Collection actualMetrics = metricReader.collectAllMetrics();
              +
              +      assertThat(actualMetrics).isNotEmpty();
              +      MetricData metricData =
              +          actualMetrics.stream()
              +              .filter(metricData1 -> metricData1.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Incorrect metric name"));
              +      assertThat(metricData.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT);
              +
              +      assertThat(
              +              metricData
              +                  .getHistogramData()
              +                  .getPoints()
              +                  .iterator()
              +                  .next()
              +                  .getAttributes()
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_ADDRESS);
              +    }
              +  }
              +}
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java
              new file mode 100644
              index 0000000000..5385ecc677
              --- /dev/null
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java
              @@ -0,0 +1,733 @@
              +/*
              + * 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 static java.nio.charset.StandardCharsets.UTF_8;
              +import static org.junit.Assert.assertThrows;
              +
              +import com.google.api.client.http.HttpTransport;
              +import com.google.api.gax.core.NoCredentialsProvider;
              +import com.google.api.gax.retrying.RetrySettings;
              +import com.google.api.gax.rpc.StatusCode;
              +import com.google.api.gax.rpc.TransportChannelProvider;
              +import com.google.api.gax.rpc.UnavailableException;
              +import com.google.api.gax.tracing.ObservabilityAttributes;
              +import com.google.api.gax.tracing.OpenTelemetryMetricsFactory;
              +import com.google.common.collect.ImmutableList;
              +import com.google.showcase.v1beta1.EchoClient;
              +import com.google.showcase.v1beta1.EchoRequest;
              +import com.google.showcase.v1beta1.EchoSettings;
              +import com.google.showcase.v1beta1.it.util.TestClientInitializer;
              +import com.google.showcase.v1beta1.stub.EchoStubSettings;
              +import io.grpc.CallOptions;
              +import io.grpc.Channel;
              +import io.grpc.ClientCall;
              +import io.grpc.ClientInterceptor;
              +import io.grpc.Metadata;
              +import io.grpc.MethodDescriptor;
              +import io.opentelemetry.api.common.AttributeKey;
              +import io.opentelemetry.sdk.OpenTelemetrySdk;
              +import io.opentelemetry.sdk.metrics.SdkMeterProvider;
              +import io.opentelemetry.sdk.metrics.data.MetricData;
              +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
              +import java.io.ByteArrayInputStream;
              +import java.io.InputStream;
              +import java.time.Duration;
              +import java.util.Collection;
              +import org.junit.jupiter.api.AfterEach;
              +import org.junit.jupiter.api.BeforeEach;
              +import org.junit.jupiter.api.Test;
              +
              +class ITOtelGoldenMetrics {
              +  private static final String SHOWCASE_SERVER_ADDRESS = "localhost";
              +  private static final long SHOWCASE_SERVER_PORT = 7469;
              +  private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase";
              +
              +  private InMemoryMetricReader metricReader;
              +  private OpenTelemetrySdk openTelemetrySdk;
              +
              +  @BeforeEach
              +  void setup() {
              +    metricReader = InMemoryMetricReader.create();
              +
              +    SdkMeterProvider meterProvider =
              +        SdkMeterProvider.builder().registerMetricReader(metricReader).build();
              +
              +    openTelemetrySdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build();
              +  }
              +
              +  @AfterEach
              +  void tearDown() {
              +    if (openTelemetrySdk != null) {
              +      openTelemetrySdk.close();
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_successfulEcho_grpc() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(tracerFactory)) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
              +
              +      // The end of an operation is tracked in a separate thread.
              +      // Add a small sleep to make sure the tracking is completed.
              +      // This is implemented by adding a TraceFinisher to ApiFuture as a callback in
              +      // TracedUnaryCallable,
              +      // which could be executed in a different thread.
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      assertThat(durationMetric.getInstrumentationScopeInfo().getName())
              +          .isEqualTo(SHOWCASE_ARTIFACT);
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_ADDRESS);
              +      assertThat(
              +              attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_PORT);
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE)))
              +          .isEqualTo("grpc");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
              +          .isEqualTo("showcase");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
              +          .isEqualTo("google.showcase.v1beta1.Echo/Echo");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("OK");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_failedEcho_grpc_recordsErrorType() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    ClientInterceptor interceptor =
              +        new ClientInterceptor() {
              +          @Override
              +          public  ClientCall interceptCall(
              +              MethodDescriptor method, CallOptions callOptions, Channel next) {
              +            return new ClientCall() {
              +              @Override
              +              public void start(Listener responseListener, Metadata headers) {
              +                responseListener.onClose(io.grpc.Status.UNAVAILABLE, new Metadata());
              +              }
              +
              +              @Override
              +              public void request(int numMessages) {}
              +
              +              @Override
              +              public void cancel(String message, Throwable cause) {}
              +
              +              @Override
              +              public void halfClose() {}
              +
              +              @Override
              +              public void sendMessage(ReqT message) {}
              +            };
              +          }
              +        };
              +
              +    TransportChannelProvider transportChannelProvider =
              +        EchoSettings.defaultGrpcTransportProviderBuilder()
              +            .setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext)
              +            .setInterceptorProvider(() -> ImmutableList.of(interceptor))
              +            .build();
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(
              +            tracerFactory, transportChannelProvider)) {
              +
              +      assertThrows(
              +          UnavailableException.class,
              +          () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("UNAVAILABLE");
              +      assertThat(
              +              attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isEqualTo("UNAVAILABLE");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_successfulEcho_httpjson() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracerFactory)) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      assertThat(durationMetric.getInstrumentationScopeInfo().getName())
              +          .isEqualTo(SHOWCASE_ARTIFACT);
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_ADDRESS);
              +      assertThat(
              +              attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_SERVER_PORT);
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE)))
              +          .isEqualTo("http");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
              +          .isEqualTo("showcase");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("OK");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo(200L);
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.URL_TEMPLATE_ATTRIBUTE)))
              +          .isEqualTo("v1beta1/echo:echo");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
              +          .isEqualTo("google.showcase.v1beta1.Echo/Echo");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_failedEcho_httpjson_recordsErrorType() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    HttpTransport mockTransport =
              +        new HttpTransport() {
              +          @Override
              +          protected com.google.api.client.http.LowLevelHttpRequest buildRequest(
              +              String method, String url) {
              +            return new com.google.api.client.http.LowLevelHttpRequest() {
              +              @Override
              +              public void addHeader(String name, String value) {}
              +
              +              @Override
              +              public com.google.api.client.http.LowLevelHttpResponse execute() {
              +                return new com.google.api.client.http.LowLevelHttpResponse() {
              +                  @Override
              +                  public InputStream getContent() {
              +                    return new ByteArrayInputStream("{}".getBytes());
              +                  }
              +
              +                  @Override
              +                  public String getContentEncoding() {
              +                    return null;
              +                  }
              +
              +                  @Override
              +                  public long getContentLength() {
              +                    return 2;
              +                  }
              +
              +                  @Override
              +                  public String getContentType() {
              +                    return "application/json";
              +                  }
              +
              +                  @Override
              +                  public String getStatusLine() {
              +                    return "HTTP/1.1 503 Service Unavailable";
              +                  }
              +
              +                  @Override
              +                  public int getStatusCode() {
              +                    return 503;
              +                  }
              +
              +                  @Override
              +                  public String getReasonPhrase() {
              +                    return "Service Unavailable";
              +                  }
              +
              +                  @Override
              +                  public int getHeaderCount() {
              +                    return 0;
              +                  }
              +
              +                  @Override
              +                  public String getHeaderName(int index) {
              +                    return null;
              +                  }
              +
              +                  @Override
              +                  public String getHeaderValue(int index) {
              +                    return null;
              +                  }
              +                };
              +              }
              +            };
              +          }
              +        };
              +
              +    EchoSettings httpJsonEchoSettings =
              +        EchoSettings.newHttpJsonBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setHttpTransport(mockTransport)
              +                    .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
              +                    .build())
              +            .build();
              +
              +    EchoStubSettings echoStubSettings =
              +        (EchoStubSettings)
              +            httpJsonEchoSettings.getStubSettings().toBuilder()
              +                .setTracerFactory(tracerFactory)
              +                .build();
              +
              +    try (EchoClient client = EchoClient.create(echoStubSettings.createStub())) {
              +      assertThrows(
              +          UnavailableException.class,
              +          () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("UNAVAILABLE");
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo(503L);
              +      assertThat(
              +              attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isEqualTo("503");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_clientTimeout_grpc() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    // Using 1ms as 0ms might be rejected by some validation or trigger immediate failure before
              +    // metrics
              +    RetrySettings zeroRetrySettings =
              +        RetrySettings.newBuilder()
              +            .setInitialRpcTimeoutDuration(Duration.ofMillis(1))
              +            .setMaxRpcTimeoutDuration(Duration.ofMillis(1))
              +            .setTotalTimeoutDuration(Duration.ofMillis(1))
              +            .setMaxAttempts(1)
              +            .build();
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetryWithRetrySettings(
              +            tracerFactory, zeroRetrySettings)) {
              +
              +      assertThrows(
              +          Exception.class,
              +          () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("DEADLINE_EXCEEDED");
              +      assertThat(
              +              attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isEqualTo("DEADLINE_EXCEEDED");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_clientTimeout_httpjson() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    RetrySettings zeroRetrySettings =
              +        RetrySettings.newBuilder()
              +            .setInitialRpcTimeoutDuration(Duration.ofMillis(1))
              +            .setMaxRpcTimeoutDuration(Duration.ofMillis(1))
              +            .setTotalTimeoutDuration(Duration.ofMillis(1))
              +            .setMaxAttempts(1)
              +            .build();
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createHttpJsonEchoClientOpentelemetryWithRetrySettings(
              +            tracerFactory, zeroRetrySettings)) {
              +
              +      assertThrows(
              +          Exception.class,
              +          () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()));
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).isNotEmpty();
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("DEADLINE_EXCEEDED");
              +      assertThat(
              +              attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isEqualTo("504");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_retryShouldResultInOneMetric_grpc() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    RetrySettings retrySettings =
              +        RetrySettings.newBuilder()
              +            .setInitialRpcTimeoutDuration(Duration.ofMillis(5000L))
              +            .setMaxRpcTimeoutDuration(Duration.ofMillis(5000L))
              +            .setTotalTimeoutDuration(Duration.ofMillis(5000L))
              +            .setMaxAttempts(3)
              +            .build();
              +
              +    java.util.concurrent.atomic.AtomicInteger attemptCount =
              +        new java.util.concurrent.atomic.AtomicInteger(0);
              +
              +    ClientInterceptor interceptor =
              +        new ClientInterceptor() {
              +          @Override
              +          public  ClientCall interceptCall(
              +              MethodDescriptor method, CallOptions callOptions, Channel next) {
              +            int attempt = attemptCount.incrementAndGet();
              +            if (attempt <= 2) {
              +              return new ClientCall() {
              +                @Override
              +                public void start(Listener responseListener, Metadata headers) {
              +                  responseListener.onClose(io.grpc.Status.UNAVAILABLE, new Metadata());
              +                }
              +
              +                @Override
              +                public void request(int numMessages) {}
              +
              +                @Override
              +                public void cancel(String message, Throwable cause) {}
              +
              +                @Override
              +                public void halfClose() {}
              +
              +                @Override
              +                public void sendMessage(ReqT message) {}
              +              };
              +            } else {
              +              return next.newCall(method, callOptions);
              +            }
              +          }
              +        };
              +
              +    java.util.Set retryableCodes =
              +        java.util.Collections.singleton(StatusCode.Code.UNAVAILABLE);
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(
              +            tracerFactory, retrySettings, retryableCodes, ImmutableList.of(interceptor))) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
              +
              +      assertThat(attemptCount.get()).isEqualTo(3);
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).hasSize(1);
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      assertThat(durationMetric.getHistogramData().getPoints()).hasSize(1);
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("OK");
              +    }
              +  }
              +
              +  @Test
              +  void testMetrics_retryShouldResultInOneMetric_httpjson() throws Exception {
              +    OpenTelemetryMetricsFactory tracerFactory = new OpenTelemetryMetricsFactory(openTelemetrySdk);
              +
              +    RetrySettings retrySettings =
              +        RetrySettings.newBuilder()
              +            .setInitialRpcTimeoutDuration(Duration.ofMillis(5000L))
              +            .setMaxRpcTimeoutDuration(Duration.ofMillis(5000L))
              +            .setTotalTimeoutDuration(Duration.ofMillis(5000L))
              +            .setMaxAttempts(3)
              +            .build();
              +
              +    java.util.concurrent.atomic.AtomicInteger requestCount =
              +        new java.util.concurrent.atomic.AtomicInteger(0);
              +
              +    HttpTransport mockTransport =
              +        new HttpTransport() {
              +          @Override
              +          protected com.google.api.client.http.LowLevelHttpRequest buildRequest(
              +              String method, String url) {
              +            int currentCount = requestCount.incrementAndGet();
              +            return new com.google.api.client.http.LowLevelHttpRequest() {
              +              @Override
              +              public void addHeader(String name, String value) {}
              +
              +              @Override
              +              public com.google.api.client.http.LowLevelHttpResponse execute() {
              +                if (currentCount <= 2) {
              +                  return new com.google.api.client.http.LowLevelHttpResponse() {
              +                    @Override
              +                    public InputStream getContent() {
              +                      return new ByteArrayInputStream("{}".getBytes(UTF_8));
              +                    }
              +
              +                    @Override
              +                    public String getContentEncoding() {
              +                      return null;
              +                    }
              +
              +                    @Override
              +                    public long getContentLength() {
              +                      return 2;
              +                    }
              +
              +                    @Override
              +                    public String getContentType() {
              +                      return "application/json";
              +                    }
              +
              +                    @Override
              +                    public String getStatusLine() {
              +                      return "HTTP/1.1 503 Service Unavailable";
              +                    }
              +
              +                    @Override
              +                    public int getStatusCode() {
              +                      return 503;
              +                    }
              +
              +                    @Override
              +                    public String getReasonPhrase() {
              +                      return "Service Unavailable";
              +                    }
              +
              +                    @Override
              +                    public int getHeaderCount() {
              +                      return 0;
              +                    }
              +
              +                    @Override
              +                    public String getHeaderName(int index) {
              +                      return null;
              +                    }
              +
              +                    @Override
              +                    public String getHeaderValue(int index) {
              +                      return null;
              +                    }
              +                  };
              +                } else {
              +                  return new com.google.api.client.http.LowLevelHttpResponse() {
              +                    @Override
              +                    public InputStream getContent() {
              +                      return new ByteArrayInputStream(
              +                          "{\"content\":\"metrics-test\"}".getBytes(UTF_8));
              +                    }
              +
              +                    @Override
              +                    public String getContentEncoding() {
              +                      return null;
              +                    }
              +
              +                    @Override
              +                    public long getContentLength() {
              +                      return 24;
              +                    }
              +
              +                    @Override
              +                    public String getContentType() {
              +                      return "application/json";
              +                    }
              +
              +                    @Override
              +                    public String getStatusLine() {
              +                      return "HTTP/1.1 200 OK";
              +                    }
              +
              +                    @Override
              +                    public int getStatusCode() {
              +                      return 200;
              +                    }
              +
              +                    @Override
              +                    public String getReasonPhrase() {
              +                      return "OK";
              +                    }
              +
              +                    @Override
              +                    public int getHeaderCount() {
              +                      return 0;
              +                    }
              +
              +                    @Override
              +                    public String getHeaderName(int index) {
              +                      return null;
              +                    }
              +
              +                    @Override
              +                    public String getHeaderValue(int index) {
              +                      return null;
              +                    }
              +                  };
              +                }
              +              }
              +            };
              +          }
              +        };
              +
              +    java.util.Set retryableCodes =
              +        java.util.Collections.singleton(StatusCode.Code.UNAVAILABLE);
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createHttpJsonEchoClientOpentelemetry(
              +            tracerFactory, retrySettings, retryableCodes, mockTransport)) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("metrics-test").build());
              +
              +      assertThat(requestCount.get()).isEqualTo(3);
              +
              +      Thread.sleep(100);
              +      Collection metrics = metricReader.collectAllMetrics();
              +      assertThat(metrics).hasSize(1);
              +
              +      MetricData durationMetric =
              +          metrics.stream()
              +              .filter(m -> m.getName().equals("gcp.client.request.duration"))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Duration metric not found"));
              +
              +      assertThat(durationMetric.getHistogramData().getPoints()).hasSize(1);
              +
              +      io.opentelemetry.api.common.Attributes attributes =
              +          durationMetric.getHistogramData().getPoints().iterator().next().getAttributes();
              +
              +      assertThat(
              +              attributes.get(
              +                  AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("OK");
              +    }
              +  }
              +}
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracePropagation.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracePropagation.java
              new file mode 100644
              index 0000000000..2d68ee0d6f
              --- /dev/null
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelTracePropagation.java
              @@ -0,0 +1,251 @@
              +/*
              + * Copyright 2026 Google LLC
              + *
              + * Licensed under the Apache License, Version 2.0 (the "License");
              + * you may not use this file except in compliance with the License.
              + * You may obtain a copy of the License at
              + *
              + *      https://www.apache.org/licenses/LICENSE-2.0
              + *
              + * Unless required by applicable law or agreed to in writing, software
              + * distributed under the License is distributed on an "AS IS" BASIS,
              + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
              + * See the License for the specific language governing permissions and
              + * limitations under the License.
              + */
              +
              +package com.google.showcase.v1beta1.it;
              +
              +import static com.google.common.truth.Truth.assertThat;
              +
              +import com.google.api.gax.httpjson.ApiMethodDescriptor;
              +import com.google.api.gax.httpjson.ForwardingHttpJsonClientCall;
              +import com.google.api.gax.httpjson.HttpJsonCallOptions;
              +import com.google.api.gax.httpjson.HttpJsonChannel;
              +import com.google.api.gax.httpjson.HttpJsonClientCall;
              +import com.google.api.gax.httpjson.HttpJsonClientInterceptor;
              +import com.google.api.gax.httpjson.HttpJsonMetadata;
              +import com.google.api.gax.tracing.OpenTelemetryTracingFactory;
              +import com.google.common.collect.ImmutableList;
              +import com.google.showcase.v1beta1.EchoClient;
              +import com.google.showcase.v1beta1.EchoRequest;
              +import com.google.showcase.v1beta1.EchoSettings;
              +import com.google.showcase.v1beta1.it.util.TestClientInitializer;
              +import io.grpc.CallOptions;
              +import io.grpc.Channel;
              +import io.grpc.ClientCall;
              +import io.grpc.ClientInterceptor;
              +import io.grpc.ForwardingClientCall;
              +import io.grpc.ForwardingClientCallListener;
              +import io.grpc.Metadata;
              +import io.grpc.MethodDescriptor;
              +import io.opentelemetry.api.GlobalOpenTelemetry;
              +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
              +import io.opentelemetry.context.propagation.ContextPropagators;
              +import io.opentelemetry.sdk.OpenTelemetrySdk;
              +import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
              +import io.opentelemetry.sdk.trace.SdkTracerProvider;
              +import io.opentelemetry.sdk.trace.data.SpanData;
              +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
              +import java.util.List;
              +import java.util.Map;
              +import java.util.concurrent.TimeUnit;
              +import org.junit.jupiter.api.AfterAll;
              +import org.junit.jupiter.api.BeforeAll;
              +import org.junit.jupiter.api.BeforeEach;
              +import org.junit.jupiter.api.Test;
              +
              +class ITOtelTracePropagation {
              +  private static final Metadata.Key TRACEPARENT_GRPC_HEADER_KEY =
              +      Metadata.Key.of("traceparent", Metadata.ASCII_STRING_MARSHALLER);
              +  private static final String TRACEPARENT_HTTP_HEADER_KEY = "traceparent";
              +
              +  private static InMemorySpanExporter spanExporter;
              +  private static OpenTelemetrySdk openTelemetrySdk;
              +
              +  private static class GrpcResponseCapturingClientInterceptor implements ClientInterceptor {
              +    private Metadata requestHeaders;
              +    private Metadata responseTrailers;
              +
              +    @Override
              +    public  ClientCall interceptCall(
              +        MethodDescriptor method, final CallOptions callOptions, Channel next) {
              +      ClientCall call = next.newCall(method, callOptions);
              +      return new ForwardingClientCall.SimpleForwardingClientCall(call) {
              +        @Override
              +        public void start(ClientCall.Listener responseListener, Metadata headers) {
              +          requestHeaders = headers;
              +          ClientCall.Listener forwardingResponseListener =
              +              new ForwardingClientCallListener.SimpleForwardingClientCallListener(
              +                  responseListener) {
              +                @Override
              +                public void onClose(io.grpc.Status status, Metadata trailers) {
              +                  responseTrailers = trailers;
              +                  super.onClose(status, trailers);
              +                }
              +              };
              +          super.start(forwardingResponseListener, headers);
              +        }
              +      };
              +    }
              +  }
              +
              +  private static class HttpJsonResponseCapturingClientInterceptor
              +      implements HttpJsonClientInterceptor {
              +    private HttpJsonMetadata requestHeaders;
              +
              +    @Override
              +    public  HttpJsonClientCall interceptCall(
              +        ApiMethodDescriptor method,
              +        HttpJsonCallOptions callOptions,
              +        HttpJsonChannel next) {
              +      HttpJsonClientCall call = next.newCall(method, callOptions);
              +      return new ForwardingHttpJsonClientCall.SimpleForwardingHttpJsonClientCall<
              +          RequestT, ResponseT>(call) {
              +        @Override
              +        public void start(Listener responseListener, HttpJsonMetadata headers) {
              +          requestHeaders = headers;
              +          super.start(responseListener, headers);
              +        }
              +      };
              +    }
              +  }
              +
              +  private static GrpcResponseCapturingClientInterceptor grpcInterceptor;
              +  private static HttpJsonResponseCapturingClientInterceptor httpJsonInterceptor;
              +
              +  private static EchoClient grpcClient;
              +  private static EchoClient httpJsonClient;
              +
              +  @BeforeAll
              +  static void setup() throws Exception {
              +    spanExporter = InMemorySpanExporter.create();
              +
              +    SdkTracerProvider tracerProvider =
              +        SdkTracerProvider.builder()
              +            .addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
              +            .build();
              +
              +    openTelemetrySdk =
              +        OpenTelemetrySdk.builder()
              +            .setTracerProvider(tracerProvider)
              +            .setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
              +            .buildAndRegisterGlobal();
              +
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +
              +    // Create gRPC Interceptor and Client
              +    grpcInterceptor = new GrpcResponseCapturingClientInterceptor();
              +    grpcClient =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(
              +            tracingFactory,
              +            EchoSettings.defaultGrpcTransportProviderBuilder()
              +                .setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext)
              +                .setInterceptorProvider(() -> ImmutableList.of(grpcInterceptor))
              +                .build());
              +
              +    // Create HttpJson Interceptor and Client
              +    httpJsonInterceptor = new HttpJsonResponseCapturingClientInterceptor();
              +    EchoSettings httpJsonEchoSettings =
              +        EchoSettings.newHttpJsonBuilder()
              +            .setCredentialsProvider(com.google.api.gax.core.NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
              +                    .setInterceptorProvider(() -> ImmutableList.of(httpJsonInterceptor))
              +                    .build())
              +            .build();
              +
              +    com.google.showcase.v1beta1.stub.EchoStubSettings echoStubSettings =
              +        (com.google.showcase.v1beta1.stub.EchoStubSettings)
              +            httpJsonEchoSettings.getStubSettings().toBuilder()
              +                .setTracerFactory(tracingFactory)
              +                .build();
              +    com.google.showcase.v1beta1.stub.EchoStub stub = echoStubSettings.createStub();
              +    httpJsonClient = EchoClient.create(stub);
              +  }
              +
              +  @BeforeEach
              +  void cleanUp() {
              +    spanExporter.reset();
              +    grpcInterceptor.requestHeaders = null;
              +    httpJsonInterceptor.requestHeaders = null;
              +  }
              +
              +  @AfterAll
              +  static void tearDown() throws InterruptedException {
              +    grpcClient.close();
              +    httpJsonClient.close();
              +
              +    grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
              +    httpJsonClient.awaitTermination(
              +        TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
              +
              +    if (openTelemetrySdk != null) {
              +      openTelemetrySdk.close();
              +    }
              +    GlobalOpenTelemetry.resetForTest();
              +  }
              +
              +  @Test
              +  void testTracePropagation_grpc() {
              +    EchoRequest request = EchoRequest.newBuilder().setContent("test-grpc-propagation").build();
              +    grpcClient.echo(request);
              +
              +    List spans = spanExporter.getFinishedSpanItems();
              +    assertThat(spans).isNotEmpty();
              +
              +    SpanData attemptSpan =
              +        spans.stream()
              +            .filter(span -> span.getName().equals("google.showcase.v1beta1.Echo/Echo"))
              +            .findFirst()
              +            .orElseThrow(() -> new AssertionError("Attempt span not found"));
              +
              +    String expectedTraceId = attemptSpan.getSpanContext().getTraceId();
              +    String expectedSpanId = attemptSpan.getSpanContext().getSpanId();
              +    String expectedTraceFlags = attemptSpan.getSpanContext().getTraceFlags().asHex();
              +    String expectedTraceparent =
              +        "00-" + expectedTraceId + "-" + expectedSpanId + "-" + expectedTraceFlags;
              +
              +    String headerValue = grpcInterceptor.requestHeaders.get(TRACEPARENT_GRPC_HEADER_KEY);
              +    assertThat(headerValue).isNotNull();
              +    assertThat(headerValue).isEqualTo(expectedTraceparent);
              +  }
              +
              +  @Test
              +  void testTracePropagation_httpjson() {
              +    EchoRequest request = EchoRequest.newBuilder().setContent("test-http-propagation").build();
              +    httpJsonClient.echo(request);
              +
              +    List spans = spanExporter.getFinishedSpanItems();
              +    assertThat(spans).isNotEmpty();
              +
              +    // The T4 CLIENT span generated by ApiTracer (OpenTelemetryTracingTracer)
              +    SpanData attemptSpan =
              +        spans.stream()
              +            .filter(span -> span.getName().equals("POST v1beta1/echo:echo"))
              +            .findFirst()
              +            .orElseThrow(() -> new AssertionError("Attempt span not found"));
              +
              +    String expectedTraceId = attemptSpan.getSpanContext().getTraceId();
              +    String expectedSpanId = attemptSpan.getSpanContext().getSpanId();
              +    String expectedTraceFlags = attemptSpan.getSpanContext().getTraceFlags().asHex();
              +    String expectedTraceparent =
              +        "00-" + expectedTraceId + "-" + expectedSpanId + "-" + expectedTraceFlags;
              +
              +    assertThat(httpJsonInterceptor.requestHeaders).isNotNull();
              +    Map headers = httpJsonInterceptor.requestHeaders.getHeaders();
              +    String expectedHttpHeaderKey = TRACEPARENT_HTTP_HEADER_KEY;
              +    assertThat(headers).containsKey(expectedHttpHeaderKey);
              +
              +    Object headerVal = headers.get(expectedHttpHeaderKey);
              +    if (headerVal instanceof List) {
              +      @SuppressWarnings("unchecked")
              +      List traceparentHeaders = (List) headerVal;
              +      assertThat(traceparentHeaders).hasSize(1);
              +      assertThat(traceparentHeaders.get(0)).isEqualTo(expectedTraceparent);
              +    } else {
              +      assertThat(String.valueOf(headerVal)).isEqualTo(expectedTraceparent);
              +    }
              +  }
              +}
              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
              index ddaff08896..ecbc16655f 100644
              --- 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
              @@ -33,23 +33,39 @@
               import static com.google.common.truth.Truth.assertThat;
               import static org.junit.Assert.assertThrows;
               
              +import com.google.api.client.http.HttpTransport;
               import com.google.api.client.http.javanet.NetHttpTransport;
               import com.google.api.gax.core.NoCredentialsProvider;
               import com.google.api.gax.retrying.RetrySettings;
              +import com.google.api.gax.rpc.InvalidArgumentException;
               import com.google.api.gax.rpc.StatusCode;
              +import com.google.api.gax.rpc.TransportChannelProvider;
               import com.google.api.gax.rpc.UnavailableException;
               import com.google.api.gax.tracing.ObservabilityAttributes;
              -import com.google.api.gax.tracing.SpanTracer;
              -import com.google.api.gax.tracing.SpanTracerFactory;
              +import com.google.api.gax.tracing.OpenTelemetryTracingFactory;
              +import com.google.common.collect.ImmutableList;
              +import com.google.protobuf.InvalidProtocolBufferException;
              +import com.google.protobuf.Message;
               import com.google.rpc.Status;
               import com.google.showcase.v1beta1.EchoClient;
               import com.google.showcase.v1beta1.EchoRequest;
              +import com.google.showcase.v1beta1.EchoResponse;
               import com.google.showcase.v1beta1.EchoSettings;
               import com.google.showcase.v1beta1.GetUserRequest;
               import com.google.showcase.v1beta1.IdentityClient;
              +import com.google.showcase.v1beta1.IdentitySettings;
               import com.google.showcase.v1beta1.it.util.TestClientInitializer;
               import com.google.showcase.v1beta1.stub.EchoStub;
               import com.google.showcase.v1beta1.stub.EchoStubSettings;
              +import com.google.showcase.v1beta1.stub.IdentityStub;
              +import com.google.showcase.v1beta1.stub.IdentityStubSettings;
              +import io.grpc.CallOptions;
              +import io.grpc.Channel;
              +import io.grpc.ClientCall;
              +import io.grpc.ClientInterceptor;
              +import io.grpc.ManagedChannelBuilder;
              +import io.grpc.Metadata;
              +import io.grpc.MethodDescriptor;
               import io.opentelemetry.api.GlobalOpenTelemetry;
               import io.opentelemetry.api.common.AttributeKey;
               import io.opentelemetry.api.trace.SpanKind;
              @@ -58,6 +74,10 @@
               import io.opentelemetry.sdk.trace.SdkTracerProvider;
               import io.opentelemetry.sdk.trace.data.SpanData;
               import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
              +import java.io.ByteArrayInputStream;
              +import java.io.IOException;
              +import java.io.InputStream;
              +import java.nio.charset.StandardCharsets;
               import java.util.List;
               import org.junit.jupiter.api.AfterEach;
               import org.junit.jupiter.api.BeforeEach;
              @@ -66,8 +86,44 @@
               class ITOtelTracing {
                 private static final String SHOWCASE_SERVER_ADDRESS = "localhost";
                 private static final long SHOWCASE_SERVER_PORT = 7469;
              +  private static final String SHOWCASE_GRPC_ENDPOINT =
              +      String.format("%s:%s", SHOWCASE_SERVER_ADDRESS, SHOWCASE_SERVER_PORT);
              +  private static final String SHOWCASE_HTTPJSON_ENDPOINT =
              +      String.format("http://%s:%s", SHOWCASE_SERVER_ADDRESS, SHOWCASE_SERVER_PORT);
                 private static final String SHOWCASE_REPO = "googleapis/sdk-platform-java";
                 private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase";
              +  private static final String SHOWCASE_USER_URL = "http://localhost:7469/v1beta1/echo:echo";
              +
              +  // Attribute Keys
              +  private static final AttributeKey RPC_SYSTEM_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE);
              +  private static final AttributeKey RPC_RESPONSE_STATUS_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE);
              +  private static final AttributeKey HTTP_RESPONSE_STATUS_KEY =
              +      AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE);
              +  private static final AttributeKey REPO_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE);
              +  private static final AttributeKey ERROR_TYPE_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE);
              +  private static final AttributeKey EXCEPTION_TYPE_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE);
              +  private static final AttributeKey STATUS_MESSAGE_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE);
              +  private static final AttributeKey DESTINATION_RESOURCE_ID_KEY =
              +      AttributeKey.stringKey(ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE);
              +
              +  // Expected Values
              +  private static final String VALUE_GRPC = "grpc";
              +  private static final String VALUE_HTTP = "http";
              +  private static final String VALUE_OK = "OK";
              +  private static final String VALUE_TEST_USER = "//showcase.googleapis.com/users/test-user";
              +  private static final String VALUE_UNAVAILABLE = "UNAVAILABLE";
              +  private static final String VALUE_UNAVAILABLE_EXCEPTION = "UnavailableException";
              +  private static final String VALUE_SERVICE_UNAVAILABLE = "Service Unavailable";
              +  private static final String SPAN_NAME_ECHO_GRPC = "google.showcase.v1beta1.Echo/Echo";
              +  private static final String SPAN_NAME_ECHO_HTTP = "POST v1beta1/echo:echo";
              +  private static final String SPAN_NAME_GET_USER_GRPC = "google.showcase.v1beta1.Identity/GetUser";
              +  private static final String SPAN_NAME_GET_USER_HTTP = "GET v1beta1/{name=users/*}";
               
                 private InMemorySpanExporter spanExporter;
                 private OpenTelemetrySdk openTelemetrySdk;
              @@ -94,32 +150,25 @@ void tearDown() {
                 }
               
                 @Test
              -  void testTracing_successfulIdentityGetUser_grpc() throws Exception {
              -    SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk);
              +  void testTracing_successfulEcho_grpc() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
               
              -    try (IdentityClient client =
              -        TestClientInitializer.createGrpcIdentityClientOpentelemetry(tracingFactory)) {
              +    EchoSettings grpcEchoSettings = createEchoSettings(false);
              +    EchoStub stub = createStubWithServiceName(grpcEchoSettings, tracingFactory);
               
              -      try {
              -        client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build());
              -      } catch (Exception e) {
              -        // Ignored, the showcase server may not have this user, but trace is still generated.
              -      }
              +    try (EchoClient client = EchoClient.create(stub)) {
              +
              +      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.Identity/GetUser"))
              +              .filter(span -> span.getName().equals(SPAN_NAME_ECHO_GRPC))
                             .findFirst()
                             .orElseThrow(() -> new AssertionError("Incorrect span name"));
                     assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT);
              -      assertThat(
              -              attemptSpan
              -                  .getAttributes()
              -                  .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE)))
              -          .isEqualTo(SpanTracer.DEFAULT_LANGUAGE);
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              @@ -130,71 +179,66 @@ void testTracing_successfulIdentityGetUser_grpc() throws Exception {
                                 .getAttributes()
                                 .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
                         .isEqualTo(SHOWCASE_SERVER_PORT);
              +      assertThat(attemptSpan.getAttributes().get(RPC_SYSTEM_KEY)).isEqualTo(VALUE_GRPC);
              +      assertThat(attemptSpan.getAttributes().get(RPC_RESPONSE_STATUS_KEY)).isEqualTo(VALUE_OK);
              +      assertThat(attemptSpan.getAttributes().get(REPO_KEY)).isEqualTo(SHOWCASE_REPO);
              +
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE)))
              -          .isEqualTo(SHOWCASE_REPO);
              +                  .get(
              +                      AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
              +          .isEqualTo("showcase");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE)))
              -          .isEqualTo(SHOWCASE_ARTIFACT);
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
              +          .isEqualTo("google.showcase.v1beta1.Echo/Echo");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE)))
              -          .isEqualTo("grpc");
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE)))
              +          .isEqualTo("showcase.googleapis.com");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE)))
              -          .isEqualTo("google.showcase.v1beta1.Identity/GetUser");
              -      // {x-version-update-start:gapic-showcase:current}
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isNull();
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.VERSION_ATTRIBUTE)))
              -          .isEqualTo("0.0.0-SNAPSHOT");
              -      // {x-version-update-end}
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE)))
              +          .isNull();
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(
              -                      AttributeKey.stringKey(
              -                          ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE)))
              -          .isEqualTo("users/test-user");
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE)))
              +          .isNull();
              +      assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT);
                   }
                 }
               
                 @Test
              -  void testTracing_successfulIdentityGetUser_httpjson() throws Exception {
              -    SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk);
              +  void testTracing_successfulEcho_httpjson() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
               
              -    try (IdentityClient client =
              -        TestClientInitializer.createHttpJsonIdentityClientOpentelemetry(tracingFactory)) {
              +    EchoSettings httpJsonEchoSettings = createEchoSettings(true);
              +    EchoStub stub = createStubWithServiceName(httpJsonEchoSettings, tracingFactory);
               
              -      try {
              -        client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build());
              -      } catch (Exception e) {
              -        // Ignored, the showcase server may not have this user, but trace is still generated.
              -      }
              +    try (EchoClient client = EchoClient.create(stub)) {
              +
              +      client.echo(EchoRequest.newBuilder().setContent("tracing-test").build());
               
                     List spans = spanExporter.getFinishedSpanItems();
                     assertThat(spans).isNotEmpty();
               
                     SpanData attemptSpan =
                         spans.stream()
              -              .filter(span -> span.getName().equals("GET v1beta1/{name=users/*}"))
              +              .filter(span -> span.getName().equals(SPAN_NAME_ECHO_HTTP))
                             .findFirst()
                             .orElseThrow(
              -                  () -> new AssertionError("Attempt span 'GET v1beta1/{name=users/*}' not found"));
              +                  () -> new AssertionError("Attempt span 'POST v1beta1/echo:echo' not found"));
                     assertThat(attemptSpan.getKind()).isEqualTo(SpanKind.CLIENT);
              -      assertThat(
              -              attemptSpan
              -                  .getAttributes()
              -                  .get(AttributeKey.stringKey(SpanTracer.LANGUAGE_ATTRIBUTE)))
              -          .isEqualTo(SpanTracer.DEFAULT_LANGUAGE);
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              @@ -205,33 +249,123 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception {
                                 .getAttributes()
                                 .get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE)))
                         .isEqualTo(SHOWCASE_SERVER_PORT);
              +      assertThat(attemptSpan.getAttributes().get(RPC_SYSTEM_KEY)).isEqualTo(VALUE_HTTP);
              +      assertThat(attemptSpan.getAttributes().get(HTTP_RESPONSE_STATUS_KEY)).isEqualTo(200L);
              +      assertThat(attemptSpan.getAttributes().get(REPO_KEY)).isEqualTo(SHOWCASE_REPO);
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.REPO_ATTRIBUTE)))
              -          .isEqualTo(SHOWCASE_REPO);
              +                  .get(
              +                      AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE)))
              +          .isEqualTo("showcase");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.ARTIFACT_ATTRIBUTE)))
              -          .isEqualTo(SHOWCASE_ARTIFACT);
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE)))
              +          .isEqualTo("POST");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE)))
              -          .isEqualTo("GET");
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE)))
              +          .isEqualTo("v1beta1/echo:echo");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE)))
              -          .isEqualTo("v1beta1/{name=users/*}");
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.URL_DOMAIN_ATTRIBUTE)))
              +          .isEqualTo("showcase.googleapis.com");
                     assertThat(
                             attemptSpan
                                 .getAttributes()
              -                  .get(
              -                      AttributeKey.stringKey(
              -                          ObservabilityAttributes.DESTINATION_RESOURCE_ID_ATTRIBUTE)))
              -          .isEqualTo("users/test-user");
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.HTTP_URL_FULL_ATTRIBUTE)))
              +          .isEqualTo(SHOWCASE_USER_URL);
              +      assertThat(
              +              attemptSpan
              +                  .getAttributes()
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE)))
              +          .isNull();
              +      assertThat(
              +              attemptSpan
              +                  .getAttributes()
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.STATUS_MESSAGE_ATTRIBUTE)))
              +          .isNull();
              +      assertThat(
              +              attemptSpan
              +                  .getAttributes()
              +                  .get(AttributeKey.stringKey(ObservabilityAttributes.EXCEPTION_TYPE_ATTRIBUTE)))
              +          .isNull();
              +      EchoResponse fetchedEcho = EchoResponse.newBuilder().setContent("tracing-test").build();
              +      long expectedMagnitude = computeExpectedHttpJsonResponseSize(fetchedEcho);
              +      Long observedMagnitude =
              +          attemptSpan
              +              .getAttributes()
              +              .get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_BODY_SIZE));
              +      assertThat(observedMagnitude).isNotNull();
              +      assertThat(observedMagnitude).isAtLeast((long) (expectedMagnitude * (1 - 0.15)));
              +      assertThat(attemptSpan.getInstrumentationScopeInfo().getName()).isEqualTo(SHOWCASE_ARTIFACT);
              +    }
              +  }
              +
              +  private long computeExpectedHttpJsonResponseSize(Message message)
              +      throws InvalidProtocolBufferException {
              +    String jsonPayload = com.google.protobuf.util.JsonFormat.printer().print(message);
              +    return jsonPayload.getBytes(StandardCharsets.UTF_8).length;
              +  }
              +
              +  @Test
              +  void testTracing_successfulIdentityGetUser_grpc() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +
              +    IdentitySettings grpcIdentitySettings = createIdentitySettings(false);
              +    IdentityStub stub = createIdentityStubWithServiceName(grpcIdentitySettings, tracingFactory);
              +
              +    try (IdentityClient client = IdentityClient.create(stub)) {
              +
              +      try {
              +        client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build());
              +      } catch (Exception e) {
              +        // Ignored, the showcase server may not have this user, but trace is still
              +        // generated.
              +      }
              +
              +      List spans = spanExporter.getFinishedSpanItems();
              +      assertThat(spans).isNotEmpty();
              +
              +      SpanData attemptSpan =
              +          spans.stream()
              +              .filter(span -> span.getName().equals(SPAN_NAME_GET_USER_GRPC))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Incorrect span name"));
              +      assertThat(attemptSpan.getAttributes().get(DESTINATION_RESOURCE_ID_KEY))
              +          .isEqualTo(VALUE_TEST_USER);
              +    }
              +  }
              +
              +  @Test
              +  void testTracing_successfulIdentityGetUser_httpjson() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +
              +    IdentitySettings httpJsonIdentitySettings = createIdentitySettings(true);
              +    IdentityStub stub = createIdentityStubWithServiceName(httpJsonIdentitySettings, tracingFactory);
              +
              +    try (IdentityClient client = IdentityClient.create(stub)) {
              +
              +      try {
              +        client.getUser(GetUserRequest.newBuilder().setName("users/test-user").build());
              +      } catch (Exception e) {
              +        // Ignored, the showcase server may not have this user, but trace is still
              +        // generated.
              +      }
              +
              +      List spans = spanExporter.getFinishedSpanItems();
              +      assertThat(spans).isNotEmpty();
              +
              +      SpanData attemptSpan =
              +          spans.stream()
              +              .filter(span -> span.getName().equals(SPAN_NAME_GET_USER_HTTP))
              +              .findFirst()
              +              .orElseThrow(() -> new AssertionError("Incorrect span name"));
              +      assertThat(attemptSpan.getAttributes().get(DESTINATION_RESOURCE_ID_KEY))
              +          .isEqualTo(VALUE_TEST_USER);
                   }
                 }
               
              @@ -239,9 +373,12 @@ void testTracing_successfulIdentityGetUser_httpjson() throws Exception {
                 void testTracing_retry_grpc() throws Exception {
                   final int attempts = 5;
                   final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE;
              -    // A custom EchoClient is used in this test because retries have jitter, and we cannot
              -    // predict the number of attempts that are scheduled for an RPC invocation otherwise.
              -    // The custom retrySettings limit to a set number of attempts before the call gives up.
              +    // A custom EchoClient is used in this test because retries have jitter, and we
              +    // cannot
              +    // predict the number of attempts that are scheduled for an RPC invocation
              +    // otherwise.
              +    // The custom retrySettings limit to a set number of attempts before the call
              +    // gives up.
                   RetrySettings retrySettings =
                       RetrySettings.newBuilder()
                           .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L))
              @@ -258,10 +395,10 @@ void testTracing_retry_grpc() throws Exception {
                       grpcEchoSettings.toBuilder()
                           .setCredentialsProvider(NoCredentialsProvider.create())
                           .setTransportChannelProvider(EchoSettings.defaultGrpcTransportProviderBuilder().build())
              -            .setEndpoint("localhost:7469")
              +            .setEndpoint(SHOWCASE_GRPC_ENDPOINT)
                           .build();
               
              -    SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk);
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
               
                   EchoStubSettings echoStubSettings =
                       (EchoStubSettings)
              @@ -280,7 +417,8 @@ void testTracing_retry_grpc() throws Exception {
                   assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry
               
                   // This single span represents the successful retry, which has resend_count=1
              -    // The first attempt has no resend_count. The subsequent retries will have a resend_count,
              +    // The first attempt has no resend_count. The subsequent retries will have a
              +    // resend_count,
                   // starting from 1.
                   List resendCounts =
                       spans.stream()
              @@ -306,10 +444,13 @@ void testTracing_retry_grpc() throws Exception {
                 @Test
                 void testTracing_retry_httpjson() throws Exception {
                   final int attempts = 5;
              -    final StatusCode.Code statusCode = StatusCode.Code.UNAVAILABLE;
              -    // A custom EchoClient is used in this test because retries have jitter, and we cannot
              -    // predict the number of attempts that are scheduled for an RPC invocation otherwise.
              -    // The custom retrySettings limit to a set number of attempts before the call gives up.
              +    final StatusCode.Code statusCode = StatusCode.Code.INVALID_ARGUMENT;
              +    // A custom EchoClient is used in this test because retries have jitter, and we
              +    // cannot
              +    // predict the number of attempts that are scheduled for an RPC invocation
              +    // otherwise.
              +    // The custom retrySettings limit to a set number of attempts before the call
              +    // gives up.
                   RetrySettings retrySettings =
                       RetrySettings.newBuilder()
                           .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L))
              @@ -329,11 +470,11 @@ void testTracing_retry_httpjson() throws Exception {
                               EchoSettings.defaultHttpJsonTransportProviderBuilder()
                                   .setHttpTransport(
                                       new NetHttpTransport.Builder().doNotValidateCertificate().build())
              -                    .setEndpoint("http://localhost:7469")
              +                    .setEndpoint(SHOWCASE_HTTPJSON_ENDPOINT)
                                   .build())
                           .build();
               
              -    SpanTracerFactory tracingFactory = new SpanTracerFactory(openTelemetrySdk);
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
               
                   EchoStubSettings echoStubSettings =
                       (EchoStubSettings)
              @@ -348,13 +489,14 @@ void testTracing_retry_httpjson() throws Exception {
                           .setError(Status.newBuilder().setCode(statusCode.ordinal()).build())
                           .build();
               
              -    assertThrows(UnavailableException.class, () -> httpClient.echo(echoRequest));
              +    assertThrows(InvalidArgumentException.class, () -> httpClient.echo(echoRequest));
               
                   List spans = spanExporter.getFinishedSpanItems();
                   assertThat(spans).hasSize(attempts); // Expect exactly one span for the successful retry
               
                   // This single span represents the successful retry, which has resend_count=1
              -    // The first attempt has no resend_count. The subsequent retries will have a resend_count,
              +    // The first attempt has no resend_count. The subsequent retries will have a
              +    // resend_count,
                   // starting from 1.
                   List resendCounts =
                       spans.stream()
              @@ -376,4 +518,340 @@ void testTracing_retry_httpjson() throws Exception {
                           .collect(java.util.stream.Collectors.toList());
                   assertThat(resendCounts).containsExactlyElementsIn(expectedCounts).inOrder();
                 }
              +
              +  private SpanData getErrorSpan() {
              +    List spans = spanExporter.getFinishedSpanItems();
              +    assertThat(spans).isNotEmpty();
              +
              +    SpanData errorSpan =
              +        spans.stream()
              +            .filter(
              +                span ->
              +                    span.getAttributes()
              +                            .get(
              +                                AttributeKey.stringKey(
              +                                    ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))
              +                        != null)
              +            .findFirst()
              +            .orElseThrow(() -> new AssertionError("Span with error.type not found"));
              +    return errorSpan;
              +  }
              +
              +  @Test
              +  void testTracing_failedEcho_grpc_recordsErrorAttributes() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +
              +    ClientInterceptor interceptor =
              +        new ClientInterceptor() {
              +          @Override
              +          public  ClientCall interceptCall(
              +              MethodDescriptor method, CallOptions callOptions, Channel next) {
              +            return new ClientCall() {
              +              @Override
              +              public void start(Listener responseListener, Metadata headers) {
              +                responseListener.onClose(io.grpc.Status.UNAVAILABLE, new Metadata());
              +              }
              +
              +              @Override
              +              public void request(int numMessages) {}
              +
              +              @Override
              +              public void cancel(String message, Throwable cause) {}
              +
              +              @Override
              +              public void halfClose() {}
              +
              +              @Override
              +              public void sendMessage(ReqT message) {}
              +            };
              +          }
              +        };
              +
              +    TransportChannelProvider transportChannelProvider =
              +        EchoSettings.defaultGrpcTransportProviderBuilder()
              +            .setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext)
              +            .setInterceptorProvider(() -> ImmutableList.of(interceptor))
              +            .build();
              +
              +    try (EchoClient client =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(
              +            tracingFactory, transportChannelProvider)) {
              +
              +      EchoRequest echoRequest = EchoRequest.newBuilder().build();
              +
              +      assertThrows(UnavailableException.class, () -> client.echo(echoRequest));
              +      SpanData errorSpan = getErrorSpan();
              +      assertThat(errorSpan.getAttributes().get(ERROR_TYPE_KEY)).isEqualTo(VALUE_UNAVAILABLE);
              +      assertThat(errorSpan.getAttributes().get(EXCEPTION_TYPE_KEY))
              +          .isEqualTo("com.google.api.gax.rpc.UnavailableException");
              +      assertThat(errorSpan.getAttributes().get(STATUS_MESSAGE_KEY))
              +          .isEqualTo("io.grpc.StatusRuntimeException: UNAVAILABLE");
              +    }
              +  }
              +
              +  @Test
              +  void testTracing_failedEcho_httpjson_recordsErrorAttributes() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +
              +    HttpTransport mockTransport =
              +        new HttpTransport() {
              +          @Override
              +          protected com.google.api.client.http.LowLevelHttpRequest buildRequest(
              +              String method, String url) {
              +            return new com.google.api.client.http.LowLevelHttpRequest() {
              +              @Override
              +              public void addHeader(String name, String value) {}
              +
              +              @Override
              +              public com.google.api.client.http.LowLevelHttpResponse execute() {
              +                return new com.google.api.client.http.LowLevelHttpResponse() {
              +                  @Override
              +                  public InputStream getContent() {
              +                    return new ByteArrayInputStream("{}".getBytes());
              +                  }
              +
              +                  @Override
              +                  public String getContentEncoding() {
              +                    return null;
              +                  }
              +
              +                  @Override
              +                  public long getContentLength() {
              +                    return 2;
              +                  }
              +
              +                  @Override
              +                  public String getContentType() {
              +                    return "application/json";
              +                  }
              +
              +                  @Override
              +                  public String getStatusLine() {
              +                    return "HTTP/1.1 503 Service Unavailable";
              +                  }
              +
              +                  @Override
              +                  public int getStatusCode() {
              +                    return 503;
              +                  }
              +
              +                  @Override
              +                  public String getReasonPhrase() {
              +                    return "Service Unavailable";
              +                  }
              +
              +                  @Override
              +                  public int getHeaderCount() {
              +                    return 0;
              +                  }
              +
              +                  @Override
              +                  public String getHeaderName(int index) {
              +                    return null;
              +                  }
              +
              +                  @Override
              +                  public String getHeaderValue(int index) {
              +                    return null;
              +                  }
              +                };
              +              }
              +            };
              +          }
              +        };
              +
              +    EchoSettings httpJsonEchoSettings =
              +        EchoSettings.newHttpJsonBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setHttpTransport(mockTransport)
              +                    .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
              +                    .build())
              +            .build();
              +
              +    EchoStubSettings echoStubSettings =
              +        (EchoStubSettings)
              +            httpJsonEchoSettings.getStubSettings().toBuilder()
              +                .setTracerFactory(tracingFactory)
              +                .build();
              +
              +    try (EchoClient client = EchoClient.create(echoStubSettings.createStub())) {
              +      EchoRequest echoRequest = EchoRequest.newBuilder().build();
              +
              +      assertThrows(UnavailableException.class, () -> client.echo(echoRequest));
              +      SpanData errorSpan = getErrorSpan();
              +      assertThat(errorSpan.getAttributes().get(ERROR_TYPE_KEY)).isEqualTo("503");
              +      assertThat(errorSpan.getAttributes().get(EXCEPTION_TYPE_KEY))
              +          .isEqualTo("com.google.api.gax.rpc.UnavailableException");
              +      assertThat(errorSpan.getAttributes().get(STATUS_MESSAGE_KEY))
              +          .isEqualTo(VALUE_SERVICE_UNAVAILABLE);
              +    }
              +  }
              +
              +  @Test
              +  void testTracing_statusCodes_grpc() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +    EchoRequest errorRequest =
              +        EchoRequest.newBuilder()
              +            .setError(
              +                Status.newBuilder().setCode(StatusCode.Code.INVALID_ARGUMENT.ordinal()).build())
              +            .build();
              +    EchoRequest successRequest = EchoRequest.newBuilder().setContent("tracing-test").build();
              +
              +    try (EchoClient grpcClient =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(tracingFactory)) {
              +
              +      grpcClient.echo(successRequest);
              +      assertThrows(
              +          com.google.api.gax.rpc.InvalidArgumentException.class,
              +          () -> grpcClient.echo(errorRequest));
              +
              +      List spans = spanExporter.getFinishedSpanItems();
              +      assertThat(spans).hasSize(2);
              +
              +      SpanData grpcSuccessSpan = spans.get(0);
              +      assertThat(
              +              grpcSuccessSpan
              +                  .getAttributes()
              +                  .get(
              +                      AttributeKey.stringKey(
              +                          ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("OK");
              +
              +      SpanData grpcErrorSpan = spans.get(1);
              +      assertThat(
              +              grpcErrorSpan
              +                  .getAttributes()
              +                  .get(
              +                      AttributeKey.stringKey(
              +                          ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo("INVALID_ARGUMENT");
              +    }
              +  }
              +
              +  @Test
              +  void testTracing_statusCodes_httpjson() throws Exception {
              +    OpenTelemetryTracingFactory tracingFactory = new OpenTelemetryTracingFactory(openTelemetrySdk);
              +    EchoRequest errorRequest =
              +        EchoRequest.newBuilder()
              +            .setError(
              +                Status.newBuilder().setCode(StatusCode.Code.INVALID_ARGUMENT.ordinal()).build())
              +            .build();
              +    EchoRequest successRequest = EchoRequest.newBuilder().setContent("tracing-test").build();
              +
              +    try (EchoClient httpClient =
              +        TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracingFactory)) {
              +
              +      httpClient.echo(successRequest);
              +      assertThrows(
              +          com.google.api.gax.rpc.InvalidArgumentException.class,
              +          () -> httpClient.echo(errorRequest));
              +
              +      List spans = spanExporter.getFinishedSpanItems();
              +      assertThat(spans).hasSize(2);
              +
              +      SpanData httpSuccessSpan = spans.get(0);
              +      assertThat(
              +              httpSuccessSpan
              +                  .getAttributes()
              +                  .get(
              +                      AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo(200L);
              +
              +      SpanData httpErrorSpan = spans.get(1);
              +      assertThat(
              +              httpErrorSpan
              +                  .getAttributes()
              +                  .get(
              +                      AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE)))
              +          .isEqualTo((long) StatusCode.Code.INVALID_ARGUMENT.getHttpStatusCode());
              +    }
              +  }
              +
              +  private EchoSettings createEchoSettings(boolean isHttpJson) throws Exception {
              +    if (isHttpJson) {
              +      return EchoSettings.newHttpJsonBuilder()
              +          .setCredentialsProvider(NoCredentialsProvider.create())
              +          .setTransportChannelProvider(
              +              EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                  .setHttpTransport(
              +                      new NetHttpTransport.Builder().doNotValidateCertificate().build())
              +                  .build())
              +          .setEndpoint(SHOWCASE_HTTPJSON_ENDPOINT)
              +          .build();
              +    } else {
              +      return EchoSettings.newBuilder()
              +          .setCredentialsProvider(NoCredentialsProvider.create())
              +          .setTransportChannelProvider(
              +              EchoSettings.defaultGrpcTransportProviderBuilder()
              +                  .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
              +                  .build())
              +          .setEndpoint(SHOWCASE_GRPC_ENDPOINT)
              +          .build();
              +    }
              +  }
              +
              +  private EchoStub createStubWithServiceName(
              +      EchoSettings settings, OpenTelemetryTracingFactory tracingFactory) throws IOException {
              +    EchoStubSettings.Builder builder =
              +        (EchoStubSettings.Builder) settings.getStubSettings().toBuilder();
              +    builder.setTracerFactory(tracingFactory);
              +    return new ExtendedEchoStubSettings(builder).createStub();
              +  }
              +
              +  private IdentityStub createIdentityStubWithServiceName(
              +      IdentitySettings settings, OpenTelemetryTracingFactory tracingFactory) throws IOException {
              +    IdentityStubSettings.Builder builder =
              +        (IdentityStubSettings.Builder) settings.getStubSettings().toBuilder();
              +    builder.setTracerFactory(tracingFactory);
              +    return new ExtendedIdentityStubSettings(builder).createStub();
              +  }
              +
              +  private IdentitySettings createIdentitySettings(boolean isHttpJson) throws Exception {
              +    if (isHttpJson) {
              +      return IdentitySettings.newHttpJsonBuilder()
              +          .setCredentialsProvider(NoCredentialsProvider.create())
              +          .setTransportChannelProvider(
              +              IdentitySettings.defaultHttpJsonTransportProviderBuilder()
              +                  .setHttpTransport(
              +                      new NetHttpTransport.Builder().doNotValidateCertificate().build())
              +                  .build())
              +          .setEndpoint(SHOWCASE_HTTPJSON_ENDPOINT)
              +          .build();
              +    } else {
              +      return IdentitySettings.newBuilder()
              +          .setCredentialsProvider(NoCredentialsProvider.create())
              +          .setTransportChannelProvider(
              +              IdentitySettings.defaultGrpcTransportProviderBuilder()
              +                  .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
              +                  .build())
              +          .setEndpoint(SHOWCASE_GRPC_ENDPOINT)
              +          .build();
              +    }
              +  }
              +
              +  /** Custom wrapper to set a service name for showcase clients, which lack one by default. */
              +  private static class ExtendedEchoStubSettings extends EchoStubSettings {
              +    protected ExtendedEchoStubSettings(EchoStubSettings.Builder builder) throws IOException {
              +      super(builder);
              +    }
              +
              +    @Override
              +    public String getServiceName() {
              +      return "showcase";
              +    }
              +  }
              +
              +  private static class ExtendedIdentityStubSettings extends IdentityStubSettings {
              +    protected ExtendedIdentityStubSettings(IdentityStubSettings.Builder builder)
              +        throws IOException {
              +      super(builder);
              +    }
              +
              +    @Override
              +    public String getServiceName() {
              +      return "showcase";
              +    }
              +  }
               }
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java
              new file mode 100644
              index 0000000000..ed71435fb4
              --- /dev/null
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/logging/ITActionableErrorsLogging.java
              @@ -0,0 +1,297 @@
              +/*
              + * Copyright 2026 Google LLC
              + *
              + * Licensed under the Apache License, Version 2.0 (the "License");
              + * you may not use this file except in compliance with the License.
              + * You may obtain a copy of the License at
              + *
              + *      https://www.apache.org/licenses/LICENSE-2.0
              + *
              + * Unless required by applicable law or agreed to in writing, software
              + * distributed under the License is distributed on an "AS IS" BASIS,
              + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
              + * See the License for the specific language governing permissions and
              + * limitations under the License.
              + */
              +
              +package com.google.showcase.v1beta1.it.logging;
              +
              +import static com.google.common.truth.Truth.assertThat;
              +import static org.junit.jupiter.api.Assertions.assertThrows;
              +
              +import ch.qos.logback.classic.Level;
              +import ch.qos.logback.classic.spi.ILoggingEvent;
              +import com.google.api.client.http.LowLevelHttpRequest;
              +import com.google.api.client.http.LowLevelHttpResponse;
              +import com.google.api.client.testing.http.MockHttpTransport;
              +import com.google.api.client.testing.http.MockLowLevelHttpRequest;
              +import com.google.api.client.testing.http.MockLowLevelHttpResponse;
              +import com.google.api.gax.core.NoCredentialsProvider;
              +import com.google.api.gax.rpc.ApiException;
              +import com.google.api.gax.tracing.LoggingTracerFactory;
              +import com.google.api.gax.tracing.ObservabilityAttributes;
              +import com.google.protobuf.Any;
              +import com.google.rpc.ErrorInfo;
              +import com.google.rpc.Status;
              +import com.google.showcase.v1beta1.EchoClient;
              +import com.google.showcase.v1beta1.EchoRequest;
              +import com.google.showcase.v1beta1.EchoSettings;
              +import com.google.showcase.v1beta1.it.util.TestClientInitializer;
              +import java.io.IOException;
              +import java.util.HashMap;
              +import java.util.Map;
              +import java.util.concurrent.TimeUnit;
              +import org.junit.jupiter.api.AfterAll;
              +import org.junit.jupiter.api.AfterEach;
              +import org.junit.jupiter.api.BeforeAll;
              +import org.junit.jupiter.api.BeforeEach;
              +import org.junit.jupiter.api.Test;
              +import org.slf4j.LoggerFactory;
              +import org.slf4j.event.KeyValuePair;
              +
              +public class ITActionableErrorsLogging {
              +
              +  private static EchoClient grpcClient;
              +  private static EchoClient httpjsonClient;
              +  private TestAppender testAppender;
              +
              +  @BeforeAll
              +  static void createClients() throws Exception {
              +    grpcClient =
              +        TestClientInitializer.createGrpcEchoClientOpentelemetry(new LoggingTracerFactory());
              +    httpjsonClient =
              +        TestClientInitializer.createHttpJsonEchoClientOpentelemetry(new LoggingTracerFactory());
              +  }
              +
              +  @AfterAll
              +  static void destroyClients() throws InterruptedException {
              +    grpcClient.close();
              +    httpjsonClient.close();
              +
              +    grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
              +    httpjsonClient.awaitTermination(
              +        TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
              +  }
              +
              +  private TestAppender setupTestLogger(String loggerName, Level level) {
              +    TestAppender appender = new TestAppender();
              +    appender.start();
              +    org.slf4j.Logger logger = LoggerFactory.getLogger(loggerName);
              +    ((ch.qos.logback.classic.Logger) logger).setLevel(level);
              +    ((ch.qos.logback.classic.Logger) logger).addAppender(appender);
              +    return appender;
              +  }
              +
              +  @BeforeEach
              +  void setupTestLogger() {
              +    testAppender = setupTestLogger("com.google.api.gax.tracing.LoggingTracer", Level.DEBUG);
              +    testAppender.clearEvents();
              +  }
              +
              +  @AfterEach
              +  void teardownTestLogger() {
              +    if (testAppender != null) {
              +      testAppender.stop();
              +    }
              +  }
              +
              +  private Map getKvps(ILoggingEvent loggingEvent) {
              +    Map map = new HashMap<>();
              +    if (loggingEvent.getKeyValuePairs() != null) {
              +      for (KeyValuePair kvp : loggingEvent.getKeyValuePairs()) {
              +        map.put(kvp.key, kvp.value);
              +      }
              +    }
              +    return map;
              +  }
              +
              +  private EchoRequest buildErrorRequest() {
              +    ErrorInfo errorInfo =
              +        ErrorInfo.newBuilder()
              +            .setReason("TEST_REASON")
              +            .setDomain("test.googleapis.com")
              +            .putMetadata("test_metadata", "test_value")
              +            .build();
              +    Status status =
              +        Status.newBuilder()
              +            .setCode(3) // INVALID_ARGUMENT
              +            .setMessage("This is a test error")
              +            .addDetails(Any.pack(errorInfo))
              +            .build();
              +    return EchoRequest.newBuilder().setError(status).build();
              +  }
              +
              +  @Test
              +  void testHttpJson_logEmittedForLowLevelRequestFailure() throws Exception {
              +    MockHttpTransport mockTransport =
              +        new MockHttpTransport() {
              +          @Override
              +          public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
              +            return new MockLowLevelHttpRequest() {
              +              @Override
              +              public LowLevelHttpResponse execute() throws IOException {
              +                MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
              +                response.setStatusCode(409); // ABORTED
              +                response.setContentType("application/json");
              +                String jsonError =
              +                    "{\n"
              +                        + "  \"error\": {\n"
              +                        + "    \"code\": 409,\n"
              +                        + "    \"message\": \"This is a mock JSON error generated by the"
              +                        + " server\",\n"
              +                        + "    \"status\": \"ABORTED\",\n"
              +                        + "    \"details\": [\n"
              +                        + "      {\n"
              +                        + "        \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\",\n"
              +                        + "        \"reason\": \"mock_error_reason\",\n"
              +                        + "        \"domain\": \"mock.googleapis.com\",\n"
              +                        + "        \"metadata\": {\"mock_key\": \"mock_value\"}\n"
              +                        + "      }\n"
              +                        + "    ]\n"
              +                        + "  }\n"
              +                        + "}";
              +                response.setContent(jsonError);
              +                return response;
              +              }
              +            };
              +          }
              +        };
              +
              +    EchoSettings httpJsonEchoSettings =
              +        EchoSettings.newHttpJsonBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setHttpTransport(mockTransport)
              +                    .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT)
              +                    .build())
              +            .build();
              +
              +    com.google.showcase.v1beta1.stub.EchoStubSettings echoStubSettings =
              +        (com.google.showcase.v1beta1.stub.EchoStubSettings)
              +            httpJsonEchoSettings.getStubSettings().toBuilder()
              +                .setTracerFactory(new LoggingTracerFactory())
              +                .build();
              +    com.google.showcase.v1beta1.stub.EchoStub stub = echoStubSettings.createStub();
              +    EchoClient mockHttpJsonClient = EchoClient.create(stub);
              +
              +    EchoRequest request = EchoRequest.newBuilder().build();
              +    assertThrows(ApiException.class, () -> mockHttpJsonClient.echo(request));
              +
              +    assertThat(testAppender.events.size()).isAtLeast(1);
              +    ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
              +
              +    assertThat(loggingEvent.getMessage())
              +        .contains("This is a mock JSON error generated by the server");
              +
              +    Map kvps = getKvps(loggingEvent);
              +    assertThat(kvps).containsEntry(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, "http");
              +    assertThat(kvps).containsEntry(ObservabilityAttributes.HTTP_METHOD_ATTRIBUTE, "POST");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.HTTP_URL_TEMPLATE_ATTRIBUTE, "v1beta1/echo:echo");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "ABORTED");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "mock_error_reason");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, "mock.googleapis.com");
              +    assertThat(kvps)
              +        .containsEntry(
              +            ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + "mock_key", "mock_value");
              +
              +    mockHttpJsonClient.close();
              +    mockHttpJsonClient.awaitTermination(
              +        TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
              +  }
              +
              +  @Test
              +  void testHttpJson_noLogEmittedForSuccess() {
              +    EchoRequest request = EchoRequest.newBuilder().setContent("Success").build();
              +    httpjsonClient.echo(request);
              +    assertThat(testAppender.events.size()).isEqualTo(0);
              +  }
              +
              +  @Test
              +  void testHttpJson_clientLevelFailureAttributes() throws Exception {
              +    com.google.showcase.v1beta1.stub.EchoStubSettings.Builder stubSettingsBuilder =
              +        com.google.showcase.v1beta1.stub.EchoStubSettings.newHttpJsonBuilder();
              +    stubSettingsBuilder
              +        .echoSettings()
              +        .setRetrySettings(
              +            com.google.api.gax.retrying.RetrySettings.newBuilder()
              +                .setInitialRpcTimeoutDuration(java.time.Duration.ofMillis(0))
              +                .setTotalTimeoutDuration(java.time.Duration.ofMillis(0))
              +                .setMaxAttempts(1)
              +                .build());
              +    stubSettingsBuilder.setTracerFactory(new LoggingTracerFactory());
              +    stubSettingsBuilder.setCredentialsProvider(NoCredentialsProvider.create());
              +    stubSettingsBuilder.setEndpoint("localhost:1");
              +
              +    try (com.google.showcase.v1beta1.stub.EchoStub stub = stubSettingsBuilder.build().createStub();
              +        EchoClient client = EchoClient.create(stub)) {
              +      assertThrows(ApiException.class, () -> client.echo(EchoRequest.newBuilder().build()));
              +      assertThat(testAppender.events.size()).isAtLeast(1);
              +      ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
              +      Map kvps = getKvps(loggingEvent);
              +      assertThat(kvps).containsEntry(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, "http");
              +    }
              +  }
              +
              +  @Test
              +  void testGrpc_logEmittedForLowLevelRequestFailure() {
              +    EchoRequest request = buildErrorRequest();
              +    assertThrows(ApiException.class, () -> grpcClient.echo(request));
              +
              +    assertThat(testAppender.events.size()).isAtLeast(1);
              +    ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
              +    assertThat(loggingEvent.getMessage()).contains("This is a test error");
              +
              +    Map kvps = getKvps(loggingEvent);
              +    assertThat(kvps).containsEntry(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, "grpc");
              +    assertThat(kvps)
              +        .containsEntry(
              +            ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE, "google.showcase.v1beta1.Echo/Echo");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, "INVALID_ARGUMENT");
              +    assertThat(kvps).containsEntry(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, "TEST_REASON");
              +    assertThat(kvps)
              +        .containsEntry(ObservabilityAttributes.ERROR_DOMAIN_ATTRIBUTE, "test.googleapis.com");
              +    assertThat(kvps)
              +        .containsEntry(
              +            ObservabilityAttributes.ERROR_METADATA_ATTRIBUTE_PREFIX + "test_metadata",
              +            "test_value");
              +  }
              +
              +  @Test
              +  void testGrpc_noLogEmittedForSuccess() {
              +    EchoRequest request = EchoRequest.newBuilder().setContent("Success").build();
              +    grpcClient.echo(request);
              +    assertThat(testAppender.events.size()).isEqualTo(0);
              +  }
              +
              +  @Test
              +  void testGrpc_clientLevelFailureAttributes() throws Exception {
              +    com.google.showcase.v1beta1.stub.EchoStubSettings.Builder stubSettingsBuilder =
              +        com.google.showcase.v1beta1.stub.EchoStubSettings.newBuilder();
              +    stubSettingsBuilder
              +        .echoSettings()
              +        .setRetrySettings(
              +            com.google.api.gax.retrying.RetrySettings.newBuilder()
              +                .setInitialRpcTimeoutDuration(java.time.Duration.ofMillis(0))
              +                .setTotalTimeoutDuration(java.time.Duration.ofMillis(0))
              +                .setMaxAttempts(1)
              +                .build());
              +    stubSettingsBuilder.setTracerFactory(new LoggingTracerFactory());
              +    stubSettingsBuilder.setCredentialsProvider(NoCredentialsProvider.create());
              +    stubSettingsBuilder.setEndpoint("localhost:1");
              +
              +    try (com.google.showcase.v1beta1.stub.EchoStub stub = stubSettingsBuilder.build().createStub();
              +        EchoClient client = EchoClient.create(stub)) {
              +      assertThrows(ApiException.class, () -> client.echo(EchoRequest.newBuilder().build()));
              +      assertThat(testAppender.events.size()).isAtLeast(1);
              +      ILoggingEvent loggingEvent = testAppender.events.get(testAppender.events.size() - 1);
              +      Map kvps = getKvps(loggingEvent);
              +      assertThat(kvps).containsEntry(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE, "grpc");
              +    }
              +  }
              +}
              diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java
              index 39e2c60f01..d9f65674bd 100644
              --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java
              +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java
              @@ -38,6 +38,7 @@
               import com.google.showcase.v1beta1.stub.EchoStubSettings;
               import io.grpc.ClientInterceptor;
               import io.grpc.ManagedChannelBuilder;
              +import java.io.IOException;
               import java.util.List;
               import java.util.Set;
               
              @@ -307,14 +308,7 @@ public static EchoClient createGrpcEchoClientOpentelemetry(
                           .setEndpoint(DEFAULT_GRPC_ENDPOINT)
                           .build();
               
              -    EchoStubSettings echoStubSettings =
              -        (EchoStubSettings)
              -            grpcEchoSettings.getStubSettings().toBuilder()
              -                .setTracerFactory(metricsTracerFactory)
              -                .build();
              -    EchoStub stub = echoStubSettings.createStub();
              -
              -    return EchoClient.create(stub);
              +    return EchoClient.create(createStubWithServiceName(grpcEchoSettings, metricsTracerFactory));
                 }
               
                 public static EchoClient createHttpJsonEchoClientOpentelemetry(
              @@ -331,14 +325,95 @@ public static EchoClient createHttpJsonEchoClientOpentelemetry(
                                   .build())
                           .build();
               
              -    EchoStubSettings echoStubSettings =
              -        (EchoStubSettings)
              -            httpJsonEchoSettings.getStubSettings().toBuilder()
              -                .setTracerFactory(metricsTracerFactory)
              -                .build();
              -    EchoStub stub = echoStubSettings.createStub();
              +    return EchoClient.create(createStubWithServiceName(httpJsonEchoSettings, metricsTracerFactory));
              +  }
               
              -    return EchoClient.create(stub);
              +  public static EchoClient createGrpcEchoClientOpentelemetryWithRetrySettings(
              +      ApiTracerFactory metricsTracerFactory, RetrySettings retrySettings) throws Exception {
              +    EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder();
              +    grpcEchoSettingsBuilder.echoSettings().setRetrySettings(retrySettings);
              +    EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build());
              +    grpcEchoSettings =
              +        grpcEchoSettings.toBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultGrpcTransportProviderBuilder()
              +                    .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
              +                    .build())
              +            .setEndpoint(DEFAULT_GRPC_ENDPOINT)
              +            .build();
              +
              +    return EchoClient.create(createStubWithServiceName(grpcEchoSettings, metricsTracerFactory));
              +  }
              +
              +  public static EchoClient createHttpJsonEchoClientOpentelemetryWithRetrySettings(
              +      ApiTracerFactory metricsTracerFactory, RetrySettings retrySettings) throws Exception {
              +    EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder();
              +    httpJsonEchoSettingsBuilder.echoSettings().setRetrySettings(retrySettings);
              +    EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build());
              +    httpJsonEchoSettings =
              +        httpJsonEchoSettings.toBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setHttpTransport(
              +                        new NetHttpTransport.Builder().doNotValidateCertificate().build())
              +                    .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT)
              +                    .build())
              +            .build();
              +
              +    return EchoClient.create(createStubWithServiceName(httpJsonEchoSettings, metricsTracerFactory));
              +  }
              +
              +  public static EchoClient createGrpcEchoClientOpentelemetry(
              +      ApiTracerFactory metricsTracerFactory,
              +      RetrySettings retrySettings,
              +      Set retryableCodes,
              +      List interceptorList)
              +      throws Exception {
              +    EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder();
              +    grpcEchoSettingsBuilder
              +        .echoSettings()
              +        .setRetrySettings(retrySettings)
              +        .setRetryableCodes(retryableCodes);
              +    EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build());
              +    grpcEchoSettings =
              +        grpcEchoSettings.toBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultGrpcTransportProviderBuilder()
              +                    .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
              +                    .setInterceptorProvider(() -> interceptorList)
              +                    .build())
              +            .setEndpoint(DEFAULT_GRPC_ENDPOINT)
              +            .build();
              +
              +    return EchoClient.create(createStubWithServiceName(grpcEchoSettings, metricsTracerFactory));
              +  }
              +
              +  public static EchoClient createHttpJsonEchoClientOpentelemetry(
              +      ApiTracerFactory metricsTracerFactory,
              +      RetrySettings retrySettings,
              +      Set retryableCodes,
              +      com.google.api.client.http.HttpTransport transport)
              +      throws Exception {
              +    EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder();
              +    httpJsonEchoSettingsBuilder
              +        .echoSettings()
              +        .setRetrySettings(retrySettings)
              +        .setRetryableCodes(retryableCodes);
              +    EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build());
              +    httpJsonEchoSettings =
              +        httpJsonEchoSettings.toBuilder()
              +            .setCredentialsProvider(NoCredentialsProvider.create())
              +            .setTransportChannelProvider(
              +                EchoSettings.defaultHttpJsonTransportProviderBuilder()
              +                    .setHttpTransport(transport)
              +                    .setEndpoint(DEFAULT_HTTPJSON_ENDPOINT)
              +                    .build())
              +            .build();
              +
              +    return EchoClient.create(createStubWithServiceName(httpJsonEchoSettings, metricsTracerFactory));
                 }
               
                 public static IdentityClient createGrpcIdentityClientOpentelemetry(ApiTracerFactory tracerFactory)
              @@ -381,4 +456,24 @@ public static IdentityClient createHttpJsonIdentityClientOpentelemetry(
                               .build();
                   return IdentityClient.create(identityStubSettings.createStub());
                 }
              +
              +  private static EchoStub createStubWithServiceName(
              +      EchoSettings settings, ApiTracerFactory tracingFactory) throws IOException {
              +    EchoStubSettings.Builder builder =
              +        (EchoStubSettings.Builder) settings.getStubSettings().toBuilder();
              +    builder.setTracerFactory(tracingFactory);
              +    return new ExtendedEchoStubSettings(builder).createStub();
              +  }
              +
              +  /** Custom wrapper to set a service name for showcase clients, which lack one by default. */
              +  private static class ExtendedEchoStubSettings extends EchoStubSettings {
              +    protected ExtendedEchoStubSettings(EchoStubSettings.Builder builder) throws IOException {
              +      super(builder);
              +    }
              +
              +    @Override
              +    public String getServiceName() {
              +      return "showcase";
              +    }
              +  }
               }
              diff --git a/java-showcase/grpc-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceServiceGrpc.java b/java-showcase/grpc-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceServiceGrpc.java
              index 70d5338fc1..cb99d3e37c 100644
              --- a/java-showcase/grpc-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceServiceGrpc.java
              +++ b/java-showcase/grpc-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceServiceGrpc.java
              @@ -17,7 +17,14 @@
               
               import static io.grpc.MethodDescriptor.generateFullMethodName;
               
              -/** */
              +/**
              + *
              + *
              + * 
              + * A service that enables testing of unary and server streaming calls
              + * by specifying a specific, predictable sequence of responses from the service
              + * 
              + */ @io.grpc.stub.annotations.GrpcGenerated public final class SequenceServiceGrpc { @@ -365,14 +372,21 @@ public SequenceServiceFutureStub newStub( return SequenceServiceFutureStub.newStub(factory, channel); } - /** */ + /** + * + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public interface AsyncService { /** * * *
              -     * Creates a sequence.
              +     * Create a sequence of responses to be returned as unary calls
                    * 
              */ default void createSequence( @@ -386,7 +400,7 @@ default void createSequence( * * *
              -     * Creates a sequence.
              +     * Creates a sequence of responses to be returned in a server streaming call
                    * 
              */ default void createStreamingSequence( @@ -401,7 +415,8 @@ default void createStreamingSequence( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information about a
              +     * sequence of unary calls.
                    * 
              */ default void getSequenceReport( @@ -415,7 +430,8 @@ default void getSequenceReport( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information
              +     * about a sequences of responses in a server streaming call.
                    * 
              */ default void getStreamingSequenceReport( @@ -430,7 +446,7 @@ default void getStreamingSequenceReport( * * *
              -     * Attempts a sequence.
              +     * Attempts a sequence of unary responses.
                    * 
              */ default void attemptSequence( @@ -444,7 +460,8 @@ default void attemptSequence( * * *
              -     * Attempts a streaming sequence.
              +     * Attempts a server streaming call with a sequence of responses
              +     * Can be used to test retries and stream resumption logic
                    * May not function as expected in HTTP mode due to when http statuses are sent
                    * See https://github.com/googleapis/gapic-showcase/issues/1377 for more details
                    * 
              @@ -458,7 +475,14 @@ default void attemptStreamingSequence( } } - /** Base class for the server implementation of the service SequenceService. */ + /** + * Base class for the server implementation of the service SequenceService. + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public abstract static class SequenceServiceImplBase implements io.grpc.BindableService, AsyncService { @@ -468,7 +492,14 @@ public final io.grpc.ServerServiceDefinition bindService() { } } - /** A stub to allow clients to do asynchronous rpc calls to service SequenceService. */ + /** + * A stub to allow clients to do asynchronous rpc calls to service SequenceService. + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public static final class SequenceServiceStub extends io.grpc.stub.AbstractAsyncStub { private SequenceServiceStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { @@ -484,7 +515,7 @@ protected SequenceServiceStub build(io.grpc.Channel channel, io.grpc.CallOptions * * *
              -     * Creates a sequence.
              +     * Create a sequence of responses to be returned as unary calls
                    * 
              */ public void createSequence( @@ -500,7 +531,7 @@ public void createSequence( * * *
              -     * Creates a sequence.
              +     * Creates a sequence of responses to be returned in a server streaming call
                    * 
              */ public void createStreamingSequence( @@ -517,7 +548,8 @@ public void createStreamingSequence( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information about a
              +     * sequence of unary calls.
                    * 
              */ public void getSequenceReport( @@ -533,7 +565,8 @@ public void getSequenceReport( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information
              +     * about a sequences of responses in a server streaming call.
                    * 
              */ public void getStreamingSequenceReport( @@ -550,7 +583,7 @@ public void getStreamingSequenceReport( * * *
              -     * Attempts a sequence.
              +     * Attempts a sequence of unary responses.
                    * 
              */ public void attemptSequence( @@ -566,7 +599,8 @@ public void attemptSequence( * * *
              -     * Attempts a streaming sequence.
              +     * Attempts a server streaming call with a sequence of responses
              +     * Can be used to test retries and stream resumption logic
                    * May not function as expected in HTTP mode due to when http statuses are sent
                    * See https://github.com/googleapis/gapic-showcase/issues/1377 for more details
                    * 
              @@ -582,7 +616,14 @@ public void attemptStreamingSequence( } } - /** A stub to allow clients to do synchronous rpc calls to service SequenceService. */ + /** + * A stub to allow clients to do synchronous rpc calls to service SequenceService. + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public static final class SequenceServiceBlockingV2Stub extends io.grpc.stub.AbstractBlockingStub { private SequenceServiceBlockingV2Stub( @@ -600,7 +641,7 @@ protected SequenceServiceBlockingV2Stub build( * * *
              -     * Creates a sequence.
              +     * Create a sequence of responses to be returned as unary calls
                    * 
              */ public com.google.showcase.v1beta1.Sequence createSequence( @@ -613,7 +654,7 @@ public com.google.showcase.v1beta1.Sequence createSequence( * * *
              -     * Creates a sequence.
              +     * Creates a sequence of responses to be returned in a server streaming call
                    * 
              */ public com.google.showcase.v1beta1.StreamingSequence createStreamingSequence( @@ -627,7 +668,8 @@ public com.google.showcase.v1beta1.StreamingSequence createStreamingSequence( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information about a
              +     * sequence of unary calls.
                    * 
              */ public com.google.showcase.v1beta1.SequenceReport getSequenceReport( @@ -641,7 +683,8 @@ public com.google.showcase.v1beta1.SequenceReport getSequenceReport( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information
              +     * about a sequences of responses in a server streaming call.
                    * 
              */ public com.google.showcase.v1beta1.StreamingSequenceReport getStreamingSequenceReport( @@ -655,7 +698,7 @@ public com.google.showcase.v1beta1.StreamingSequenceReport getStreamingSequenceR * * *
              -     * Attempts a sequence.
              +     * Attempts a sequence of unary responses.
                    * 
              */ public com.google.protobuf.Empty attemptSequence( @@ -668,7 +711,8 @@ public com.google.protobuf.Empty attemptSequence( * * *
              -     * Attempts a streaming sequence.
              +     * Attempts a server streaming call with a sequence of responses
              +     * Can be used to test retries and stream resumption logic
                    * May not function as expected in HTTP mode due to when http statuses are sent
                    * See https://github.com/googleapis/gapic-showcase/issues/1377 for more details
                    * 
              @@ -683,7 +727,14 @@ public com.google.protobuf.Empty attemptSequence( } } - /** A stub to allow clients to do limited synchronous rpc calls to service SequenceService. */ + /** + * A stub to allow clients to do limited synchronous rpc calls to service SequenceService. + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public static final class SequenceServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub { private SequenceServiceBlockingStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { @@ -700,7 +751,7 @@ protected SequenceServiceBlockingStub build( * * *
              -     * Creates a sequence.
              +     * Create a sequence of responses to be returned as unary calls
                    * 
              */ public com.google.showcase.v1beta1.Sequence createSequence( @@ -713,7 +764,7 @@ public com.google.showcase.v1beta1.Sequence createSequence( * * *
              -     * Creates a sequence.
              +     * Creates a sequence of responses to be returned in a server streaming call
                    * 
              */ public com.google.showcase.v1beta1.StreamingSequence createStreamingSequence( @@ -726,7 +777,8 @@ public com.google.showcase.v1beta1.StreamingSequence createStreamingSequence( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information about a
              +     * sequence of unary calls.
                    * 
              */ public com.google.showcase.v1beta1.SequenceReport getSequenceReport( @@ -739,7 +791,8 @@ public com.google.showcase.v1beta1.SequenceReport getSequenceReport( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information
              +     * about a sequences of responses in a server streaming call.
                    * 
              */ public com.google.showcase.v1beta1.StreamingSequenceReport getStreamingSequenceReport( @@ -752,7 +805,7 @@ public com.google.showcase.v1beta1.StreamingSequenceReport getStreamingSequenceR * * *
              -     * Attempts a sequence.
              +     * Attempts a sequence of unary responses.
                    * 
              */ public com.google.protobuf.Empty attemptSequence( @@ -765,7 +818,8 @@ public com.google.protobuf.Empty attemptSequence( * * *
              -     * Attempts a streaming sequence.
              +     * Attempts a server streaming call with a sequence of responses
              +     * Can be used to test retries and stream resumption logic
                    * May not function as expected in HTTP mode due to when http statuses are sent
                    * See https://github.com/googleapis/gapic-showcase/issues/1377 for more details
                    * 
              @@ -778,7 +832,14 @@ public com.google.protobuf.Empty attemptSequence( } } - /** A stub to allow clients to do ListenableFuture-style rpc calls to service SequenceService. */ + /** + * A stub to allow clients to do ListenableFuture-style rpc calls to service SequenceService. + * + *
              +   * A service that enables testing of unary and server streaming calls
              +   * by specifying a specific, predictable sequence of responses from the service
              +   * 
              + */ public static final class SequenceServiceFutureStub extends io.grpc.stub.AbstractFutureStub { private SequenceServiceFutureStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) { @@ -795,7 +856,7 @@ protected SequenceServiceFutureStub build( * * *
              -     * Creates a sequence.
              +     * Create a sequence of responses to be returned as unary calls
                    * 
              */ public com.google.common.util.concurrent.ListenableFuture @@ -808,7 +869,7 @@ protected SequenceServiceFutureStub build( * * *
              -     * Creates a sequence.
              +     * Creates a sequence of responses to be returned in a server streaming call
                    * 
              */ public com.google.common.util.concurrent.ListenableFuture< @@ -823,7 +884,8 @@ protected SequenceServiceFutureStub build( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information about a
              +     * sequence of unary calls.
                    * 
              */ public com.google.common.util.concurrent.ListenableFuture< @@ -837,7 +899,8 @@ protected SequenceServiceFutureStub build( * * *
              -     * Retrieves a sequence.
              +     * Retrieves a sequence report which can be used to retrieve information
              +     * about a sequences of responses in a server streaming call.
                    * 
              */ public com.google.common.util.concurrent.ListenableFuture< @@ -852,7 +915,7 @@ protected SequenceServiceFutureStub build( * * *
              -     * Attempts a sequence.
              +     * Attempts a sequence of unary responses.
                    * 
              */ public com.google.common.util.concurrent.ListenableFuture diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptSequenceRequest.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptSequenceRequest.java index cc7b4e4d40..946f1ce668 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptSequenceRequest.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptSequenceRequest.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.AttemptSequenceRequest} */ +/** + * + * + *
              + * Request message for the unary AttemptSequence method
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.AttemptSequenceRequest} + */ @com.google.protobuf.Generated public final class AttemptSequenceRequest extends com.google.protobuf.GeneratedMessage implements @@ -267,7 +275,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.AttemptSequenceRequest} */ + /** + * + * + *
              +   * Request message for the unary AttemptSequence method
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.AttemptSequenceRequest} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.AttemptSequenceRequest) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceRequest.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceRequest.java index 736aa10f95..88bef5ff3a 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceRequest.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceRequest.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceRequest} */ +/** + * + * + *
              + * Request message for the AttemptStreamingSequence method.
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceRequest} + */ @com.google.protobuf.Generated public final class AttemptStreamingSequenceRequest extends com.google.protobuf.GeneratedMessage implements @@ -298,7 +306,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceRequest} */ + /** + * + * + *
              +   * Request message for the AttemptStreamingSequence method.
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceRequest} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.AttemptStreamingSequenceRequest) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceResponse.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceResponse.java index 7812b3e6c2..e332ab1cce 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceResponse.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/AttemptStreamingSequenceResponse.java @@ -24,7 +24,7 @@ * * *
              - * The response message for the Echo methods.
              + * The response message for the AttemptStreamingSequence method.
                * 
              * * Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceResponse} @@ -289,7 +289,7 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder * * *
              -   * The response message for the Echo methods.
              +   * The response message for the AttemptStreamingSequence method.
                  * 
              * * Protobuf type {@code google.showcase.v1beta1.AttemptStreamingSequenceResponse} diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceData.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceData.java index a9ca21258d..51cf60cc0e 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceData.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceData.java @@ -574,6 +574,236 @@ public int getPInt32() { return pInt32_; } + public static final int P_SINT32_FIELD_NUMBER = 39; + private int pSint32_ = 0; + + /** + * optional sint32 p_sint32 = 39; + * + * @return Whether the pSint32 field is set. + */ + @java.lang.Override + public boolean hasPSint32() { + return ((bitField0_ & 0x00000008) != 0); + } + + /** + * optional sint32 p_sint32 = 39; + * + * @return The pSint32. + */ + @java.lang.Override + public int getPSint32() { + return pSint32_; + } + + public static final int P_SFIXED32_FIELD_NUMBER = 40; + private int pSfixed32_ = 0; + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return Whether the pSfixed32 field is set. + */ + @java.lang.Override + public boolean hasPSfixed32() { + return ((bitField0_ & 0x00000010) != 0); + } + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return The pSfixed32. + */ + @java.lang.Override + public int getPSfixed32() { + return pSfixed32_; + } + + public static final int P_UINT32_FIELD_NUMBER = 41; + private int pUint32_ = 0; + + /** + * optional uint32 p_uint32 = 41; + * + * @return Whether the pUint32 field is set. + */ + @java.lang.Override + public boolean hasPUint32() { + return ((bitField0_ & 0x00000020) != 0); + } + + /** + * optional uint32 p_uint32 = 41; + * + * @return The pUint32. + */ + @java.lang.Override + public int getPUint32() { + return pUint32_; + } + + public static final int P_FIXED32_FIELD_NUMBER = 42; + private int pFixed32_ = 0; + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return Whether the pFixed32 field is set. + */ + @java.lang.Override + public boolean hasPFixed32() { + return ((bitField0_ & 0x00000040) != 0); + } + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return The pFixed32. + */ + @java.lang.Override + public int getPFixed32() { + return pFixed32_; + } + + public static final int P_INT64_FIELD_NUMBER = 43; + private long pInt64_ = 0L; + + /** + * optional int64 p_int64 = 43; + * + * @return Whether the pInt64 field is set. + */ + @java.lang.Override + public boolean hasPInt64() { + return ((bitField0_ & 0x00000080) != 0); + } + + /** + * optional int64 p_int64 = 43; + * + * @return The pInt64. + */ + @java.lang.Override + public long getPInt64() { + return pInt64_; + } + + public static final int P_SINT64_FIELD_NUMBER = 44; + private long pSint64_ = 0L; + + /** + * optional sint64 p_sint64 = 44; + * + * @return Whether the pSint64 field is set. + */ + @java.lang.Override + public boolean hasPSint64() { + return ((bitField0_ & 0x00000100) != 0); + } + + /** + * optional sint64 p_sint64 = 44; + * + * @return The pSint64. + */ + @java.lang.Override + public long getPSint64() { + return pSint64_; + } + + public static final int P_SFIXED64_FIELD_NUMBER = 45; + private long pSfixed64_ = 0L; + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return Whether the pSfixed64 field is set. + */ + @java.lang.Override + public boolean hasPSfixed64() { + return ((bitField0_ & 0x00000200) != 0); + } + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return The pSfixed64. + */ + @java.lang.Override + public long getPSfixed64() { + return pSfixed64_; + } + + public static final int P_UINT64_FIELD_NUMBER = 46; + private long pUint64_ = 0L; + + /** + * optional uint64 p_uint64 = 46; + * + * @return Whether the pUint64 field is set. + */ + @java.lang.Override + public boolean hasPUint64() { + return ((bitField0_ & 0x00000400) != 0); + } + + /** + * optional uint64 p_uint64 = 46; + * + * @return The pUint64. + */ + @java.lang.Override + public long getPUint64() { + return pUint64_; + } + + public static final int P_FIXED64_FIELD_NUMBER = 47; + private long pFixed64_ = 0L; + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return Whether the pFixed64 field is set. + */ + @java.lang.Override + public boolean hasPFixed64() { + return ((bitField0_ & 0x00000800) != 0); + } + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return The pFixed64. + */ + @java.lang.Override + public long getPFixed64() { + return pFixed64_; + } + + public static final int P_FLOAT_FIELD_NUMBER = 48; + private float pFloat_ = 0F; + + /** + * optional float p_float = 48; + * + * @return Whether the pFloat field is set. + */ + @java.lang.Override + public boolean hasPFloat() { + return ((bitField0_ & 0x00001000) != 0); + } + + /** + * optional float p_float = 48; + * + * @return The pFloat. + */ + @java.lang.Override + public float getPFloat() { + return pFloat_; + } + public static final int P_DOUBLE_FIELD_NUMBER = 19; private double pDouble_ = 0D; @@ -584,7 +814,7 @@ public int getPInt32() { */ @java.lang.Override public boolean hasPDouble() { - return ((bitField0_ & 0x00000008) != 0); + return ((bitField0_ & 0x00002000) != 0); } /** @@ -607,7 +837,7 @@ public double getPDouble() { */ @java.lang.Override public boolean hasPBool() { - return ((bitField0_ & 0x00000010) != 0); + return ((bitField0_ & 0x00004000) != 0); } /** @@ -630,7 +860,7 @@ public boolean getPBool() { */ @java.lang.Override public boolean hasPKingdom() { - return ((bitField0_ & 0x00000020) != 0); + return ((bitField0_ & 0x00008000) != 0); } /** @@ -667,7 +897,7 @@ public com.google.showcase.v1beta1.ComplianceData.LifeKingdom getPKingdom() { */ @java.lang.Override public boolean hasPChild() { - return ((bitField0_ & 0x00000040) != 0); + return ((bitField0_ & 0x00010000) != 0); } /** @@ -758,13 +988,13 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io if (((bitField0_ & 0x00000004) != 0)) { output.writeInt32(18, pInt32_); } - if (((bitField0_ & 0x00000008) != 0)) { + if (((bitField0_ & 0x00002000) != 0)) { output.writeDouble(19, pDouble_); } - if (((bitField0_ & 0x00000010) != 0)) { + if (((bitField0_ & 0x00004000) != 0)) { output.writeBool(20, pBool_); } - if (((bitField0_ & 0x00000040) != 0)) { + if (((bitField0_ & 0x00010000) != 0)) { output.writeMessage(21, getPChild()); } if (fKingdom_ @@ -772,9 +1002,39 @@ public void writeTo(com.google.protobuf.CodedOutputStream output) throws java.io .getNumber()) { output.writeEnum(22, fKingdom_); } - if (((bitField0_ & 0x00000020) != 0)) { + if (((bitField0_ & 0x00008000) != 0)) { output.writeEnum(23, pKingdom_); } + if (((bitField0_ & 0x00000008) != 0)) { + output.writeSInt32(39, pSint32_); + } + if (((bitField0_ & 0x00000010) != 0)) { + output.writeSFixed32(40, pSfixed32_); + } + if (((bitField0_ & 0x00000020) != 0)) { + output.writeUInt32(41, pUint32_); + } + if (((bitField0_ & 0x00000040) != 0)) { + output.writeFixed32(42, pFixed32_); + } + if (((bitField0_ & 0x00000080) != 0)) { + output.writeInt64(43, pInt64_); + } + if (((bitField0_ & 0x00000100) != 0)) { + output.writeSInt64(44, pSint64_); + } + if (((bitField0_ & 0x00000200) != 0)) { + output.writeSFixed64(45, pSfixed64_); + } + if (((bitField0_ & 0x00000400) != 0)) { + output.writeUInt64(46, pUint64_); + } + if (((bitField0_ & 0x00000800) != 0)) { + output.writeFixed64(47, pFixed64_); + } + if (((bitField0_ & 0x00001000) != 0)) { + output.writeFloat(48, pFloat_); + } getUnknownFields().writeTo(output); } @@ -838,13 +1098,13 @@ public int getSerializedSize() { if (((bitField0_ & 0x00000004) != 0)) { size += com.google.protobuf.CodedOutputStream.computeInt32Size(18, pInt32_); } - if (((bitField0_ & 0x00000008) != 0)) { + if (((bitField0_ & 0x00002000) != 0)) { size += com.google.protobuf.CodedOutputStream.computeDoubleSize(19, pDouble_); } - if (((bitField0_ & 0x00000010) != 0)) { + if (((bitField0_ & 0x00004000) != 0)) { size += com.google.protobuf.CodedOutputStream.computeBoolSize(20, pBool_); } - if (((bitField0_ & 0x00000040) != 0)) { + if (((bitField0_ & 0x00010000) != 0)) { size += com.google.protobuf.CodedOutputStream.computeMessageSize(21, getPChild()); } if (fKingdom_ @@ -852,9 +1112,39 @@ public int getSerializedSize() { .getNumber()) { size += com.google.protobuf.CodedOutputStream.computeEnumSize(22, fKingdom_); } - if (((bitField0_ & 0x00000020) != 0)) { + if (((bitField0_ & 0x00008000) != 0)) { size += com.google.protobuf.CodedOutputStream.computeEnumSize(23, pKingdom_); } + if (((bitField0_ & 0x00000008) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeSInt32Size(39, pSint32_); + } + if (((bitField0_ & 0x00000010) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeSFixed32Size(40, pSfixed32_); + } + if (((bitField0_ & 0x00000020) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeUInt32Size(41, pUint32_); + } + if (((bitField0_ & 0x00000040) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeFixed32Size(42, pFixed32_); + } + if (((bitField0_ & 0x00000080) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeInt64Size(43, pInt64_); + } + if (((bitField0_ & 0x00000100) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeSInt64Size(44, pSint64_); + } + if (((bitField0_ & 0x00000200) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeSFixed64Size(45, pSfixed64_); + } + if (((bitField0_ & 0x00000400) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeUInt64Size(46, pUint64_); + } + if (((bitField0_ & 0x00000800) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeFixed64Size(47, pFixed64_); + } + if (((bitField0_ & 0x00001000) != 0)) { + size += com.google.protobuf.CodedOutputStream.computeFloatSize(48, pFloat_); + } size += getUnknownFields().getSerializedSize(); memoizedSize = size; return size; @@ -901,6 +1191,47 @@ public boolean equals(final java.lang.Object obj) { if (hasPInt32()) { if (getPInt32() != other.getPInt32()) return false; } + if (hasPSint32() != other.hasPSint32()) return false; + if (hasPSint32()) { + if (getPSint32() != other.getPSint32()) return false; + } + if (hasPSfixed32() != other.hasPSfixed32()) return false; + if (hasPSfixed32()) { + if (getPSfixed32() != other.getPSfixed32()) return false; + } + if (hasPUint32() != other.hasPUint32()) return false; + if (hasPUint32()) { + if (getPUint32() != other.getPUint32()) return false; + } + if (hasPFixed32() != other.hasPFixed32()) return false; + if (hasPFixed32()) { + if (getPFixed32() != other.getPFixed32()) return false; + } + if (hasPInt64() != other.hasPInt64()) return false; + if (hasPInt64()) { + if (getPInt64() != other.getPInt64()) return false; + } + if (hasPSint64() != other.hasPSint64()) return false; + if (hasPSint64()) { + if (getPSint64() != other.getPSint64()) return false; + } + if (hasPSfixed64() != other.hasPSfixed64()) return false; + if (hasPSfixed64()) { + if (getPSfixed64() != other.getPSfixed64()) return false; + } + if (hasPUint64() != other.hasPUint64()) return false; + if (hasPUint64()) { + if (getPUint64() != other.getPUint64()) return false; + } + if (hasPFixed64() != other.hasPFixed64()) return false; + if (hasPFixed64()) { + if (getPFixed64() != other.getPFixed64()) return false; + } + if (hasPFloat() != other.hasPFloat()) return false; + if (hasPFloat()) { + if (java.lang.Float.floatToIntBits(getPFloat()) + != java.lang.Float.floatToIntBits(other.getPFloat())) return false; + } if (hasPDouble() != other.hasPDouble()) return false; if (hasPDouble()) { if (java.lang.Double.doubleToLongBits(getPDouble()) @@ -976,6 +1307,46 @@ public int hashCode() { hash = (37 * hash) + P_INT32_FIELD_NUMBER; hash = (53 * hash) + getPInt32(); } + if (hasPSint32()) { + hash = (37 * hash) + P_SINT32_FIELD_NUMBER; + hash = (53 * hash) + getPSint32(); + } + if (hasPSfixed32()) { + hash = (37 * hash) + P_SFIXED32_FIELD_NUMBER; + hash = (53 * hash) + getPSfixed32(); + } + if (hasPUint32()) { + hash = (37 * hash) + P_UINT32_FIELD_NUMBER; + hash = (53 * hash) + getPUint32(); + } + if (hasPFixed32()) { + hash = (37 * hash) + P_FIXED32_FIELD_NUMBER; + hash = (53 * hash) + getPFixed32(); + } + if (hasPInt64()) { + hash = (37 * hash) + P_INT64_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getPInt64()); + } + if (hasPSint64()) { + hash = (37 * hash) + P_SINT64_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getPSint64()); + } + if (hasPSfixed64()) { + hash = (37 * hash) + P_SFIXED64_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getPSfixed64()); + } + if (hasPUint64()) { + hash = (37 * hash) + P_UINT64_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getPUint64()); + } + if (hasPFixed64()) { + hash = (37 * hash) + P_FIXED64_FIELD_NUMBER; + hash = (53 * hash) + com.google.protobuf.Internal.hashLong(getPFixed64()); + } + if (hasPFloat()) { + hash = (37 * hash) + P_FLOAT_FIELD_NUMBER; + hash = (53 * hash) + java.lang.Float.floatToIntBits(getPFloat()); + } if (hasPDouble()) { hash = (37 * hash) + P_DOUBLE_FIELD_NUMBER; hash = @@ -1146,6 +1517,7 @@ private void maybeForceBuilderInitialization() { public Builder clear() { super.clear(); bitField0_ = 0; + bitField1_ = 0; fString_ = ""; fInt32_ = 0; fSint32_ = 0; @@ -1169,6 +1541,16 @@ public Builder clear() { } pString_ = ""; pInt32_ = 0; + pSint32_ = 0; + pSfixed32_ = 0; + pUint32_ = 0; + pFixed32_ = 0; + pInt64_ = 0L; + pSint64_ = 0L; + pSfixed64_ = 0L; + pUint64_ = 0L; + pFixed64_ = 0L; + pFloat_ = 0F; pDouble_ = 0D; pBool_ = false; pKingdom_ = 0; @@ -1207,6 +1589,9 @@ public com.google.showcase.v1beta1.ComplianceData buildPartial() { if (bitField0_ != 0) { buildPartial0(result); } + if (bitField1_ != 0) { + buildPartial1(result); + } onBuilt(); return result; } @@ -1275,21 +1660,67 @@ private void buildPartial0(com.google.showcase.v1beta1.ComplianceData result) { to_bitField0_ |= 0x00000004; } if (((from_bitField0_ & 0x00080000) != 0)) { - result.pDouble_ = pDouble_; + result.pSint32_ = pSint32_; to_bitField0_ |= 0x00000008; } if (((from_bitField0_ & 0x00100000) != 0)) { - result.pBool_ = pBool_; + result.pSfixed32_ = pSfixed32_; to_bitField0_ |= 0x00000010; } if (((from_bitField0_ & 0x00200000) != 0)) { - result.pKingdom_ = pKingdom_; + result.pUint32_ = pUint32_; to_bitField0_ |= 0x00000020; } if (((from_bitField0_ & 0x00400000) != 0)) { - result.pChild_ = pChildBuilder_ == null ? pChild_ : pChildBuilder_.build(); + result.pFixed32_ = pFixed32_; to_bitField0_ |= 0x00000040; } + if (((from_bitField0_ & 0x00800000) != 0)) { + result.pInt64_ = pInt64_; + to_bitField0_ |= 0x00000080; + } + if (((from_bitField0_ & 0x01000000) != 0)) { + result.pSint64_ = pSint64_; + to_bitField0_ |= 0x00000100; + } + if (((from_bitField0_ & 0x02000000) != 0)) { + result.pSfixed64_ = pSfixed64_; + to_bitField0_ |= 0x00000200; + } + if (((from_bitField0_ & 0x04000000) != 0)) { + result.pUint64_ = pUint64_; + to_bitField0_ |= 0x00000400; + } + if (((from_bitField0_ & 0x08000000) != 0)) { + result.pFixed64_ = pFixed64_; + to_bitField0_ |= 0x00000800; + } + if (((from_bitField0_ & 0x10000000) != 0)) { + result.pFloat_ = pFloat_; + to_bitField0_ |= 0x00001000; + } + if (((from_bitField0_ & 0x20000000) != 0)) { + result.pDouble_ = pDouble_; + to_bitField0_ |= 0x00002000; + } + if (((from_bitField0_ & 0x40000000) != 0)) { + result.pBool_ = pBool_; + to_bitField0_ |= 0x00004000; + } + if (((from_bitField0_ & 0x80000000) != 0)) { + result.pKingdom_ = pKingdom_; + to_bitField0_ |= 0x00008000; + } + result.bitField0_ |= to_bitField0_; + } + + private void buildPartial1(com.google.showcase.v1beta1.ComplianceData result) { + int from_bitField1_ = bitField1_; + int to_bitField0_ = 0; + if (((from_bitField1_ & 0x00000001) != 0)) { + result.pChild_ = pChildBuilder_ == null ? pChild_ : pChildBuilder_.build(); + to_bitField0_ |= 0x00010000; + } result.bitField0_ |= to_bitField0_; } @@ -1366,6 +1797,36 @@ public Builder mergeFrom(com.google.showcase.v1beta1.ComplianceData other) { if (other.hasPInt32()) { setPInt32(other.getPInt32()); } + if (other.hasPSint32()) { + setPSint32(other.getPSint32()); + } + if (other.hasPSfixed32()) { + setPSfixed32(other.getPSfixed32()); + } + if (other.hasPUint32()) { + setPUint32(other.getPUint32()); + } + if (other.hasPFixed32()) { + setPFixed32(other.getPFixed32()); + } + if (other.hasPInt64()) { + setPInt64(other.getPInt64()); + } + if (other.hasPSint64()) { + setPSint64(other.getPSint64()); + } + if (other.hasPSfixed64()) { + setPSfixed64(other.getPSfixed64()); + } + if (other.hasPUint64()) { + setPUint64(other.getPUint64()); + } + if (other.hasPFixed64()) { + setPFixed64(other.getPFixed64()); + } + if (other.hasPFloat()) { + setPFloat(other.getPFloat()); + } if (other.hasPDouble()) { setPDouble(other.getPDouble()); } @@ -1515,19 +1976,19 @@ public Builder mergeFrom( case 153: { pDouble_ = input.readDouble(); - bitField0_ |= 0x00080000; + bitField0_ |= 0x20000000; break; } // case 153 case 160: { pBool_ = input.readBool(); - bitField0_ |= 0x00100000; + bitField0_ |= 0x40000000; break; } // case 160 case 170: { input.readMessage(internalGetPChildFieldBuilder().getBuilder(), extensionRegistry); - bitField0_ |= 0x00400000; + bitField1_ |= 0x00000001; break; } // case 170 case 176: @@ -1539,9 +2000,69 @@ public Builder mergeFrom( case 184: { pKingdom_ = input.readEnum(); - bitField0_ |= 0x00200000; + bitField0_ |= 0x80000000; break; } // case 184 + case 312: + { + pSint32_ = input.readSInt32(); + bitField0_ |= 0x00080000; + break; + } // case 312 + case 325: + { + pSfixed32_ = input.readSFixed32(); + bitField0_ |= 0x00100000; + break; + } // case 325 + case 328: + { + pUint32_ = input.readUInt32(); + bitField0_ |= 0x00200000; + break; + } // case 328 + case 341: + { + pFixed32_ = input.readFixed32(); + bitField0_ |= 0x00400000; + break; + } // case 341 + case 344: + { + pInt64_ = input.readInt64(); + bitField0_ |= 0x00800000; + break; + } // case 344 + case 352: + { + pSint64_ = input.readSInt64(); + bitField0_ |= 0x01000000; + break; + } // case 352 + case 361: + { + pSfixed64_ = input.readSFixed64(); + bitField0_ |= 0x02000000; + break; + } // case 361 + case 368: + { + pUint64_ = input.readUInt64(); + bitField0_ |= 0x04000000; + break; + } // case 368 + case 377: + { + pFixed64_ = input.readFixed64(); + bitField0_ |= 0x08000000; + break; + } // case 377 + case 389: + { + pFloat_ = input.readFloat(); + bitField0_ |= 0x10000000; + break; + } // case 389 default: { if (!super.parseUnknownField(input, extensionRegistry, tag)) { @@ -1560,6 +2081,7 @@ public Builder mergeFrom( } private int bitField0_; + private int bitField1_; private java.lang.Object fString_ = ""; @@ -2508,7 +3030,487 @@ public Builder clearPInt32() { return this; } - private double pDouble_; + private int pSint32_; + + /** + * optional sint32 p_sint32 = 39; + * + * @return Whether the pSint32 field is set. + */ + @java.lang.Override + public boolean hasPSint32() { + return ((bitField0_ & 0x00080000) != 0); + } + + /** + * optional sint32 p_sint32 = 39; + * + * @return The pSint32. + */ + @java.lang.Override + public int getPSint32() { + return pSint32_; + } + + /** + * optional sint32 p_sint32 = 39; + * + * @param value The pSint32 to set. + * @return This builder for chaining. + */ + public Builder setPSint32(int value) { + + pSint32_ = value; + bitField0_ |= 0x00080000; + onChanged(); + return this; + } + + /** + * optional sint32 p_sint32 = 39; + * + * @return This builder for chaining. + */ + public Builder clearPSint32() { + bitField0_ = (bitField0_ & ~0x00080000); + pSint32_ = 0; + onChanged(); + return this; + } + + private int pSfixed32_; + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return Whether the pSfixed32 field is set. + */ + @java.lang.Override + public boolean hasPSfixed32() { + return ((bitField0_ & 0x00100000) != 0); + } + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return The pSfixed32. + */ + @java.lang.Override + public int getPSfixed32() { + return pSfixed32_; + } + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @param value The pSfixed32 to set. + * @return This builder for chaining. + */ + public Builder setPSfixed32(int value) { + + pSfixed32_ = value; + bitField0_ |= 0x00100000; + onChanged(); + return this; + } + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return This builder for chaining. + */ + public Builder clearPSfixed32() { + bitField0_ = (bitField0_ & ~0x00100000); + pSfixed32_ = 0; + onChanged(); + return this; + } + + private int pUint32_; + + /** + * optional uint32 p_uint32 = 41; + * + * @return Whether the pUint32 field is set. + */ + @java.lang.Override + public boolean hasPUint32() { + return ((bitField0_ & 0x00200000) != 0); + } + + /** + * optional uint32 p_uint32 = 41; + * + * @return The pUint32. + */ + @java.lang.Override + public int getPUint32() { + return pUint32_; + } + + /** + * optional uint32 p_uint32 = 41; + * + * @param value The pUint32 to set. + * @return This builder for chaining. + */ + public Builder setPUint32(int value) { + + pUint32_ = value; + bitField0_ |= 0x00200000; + onChanged(); + return this; + } + + /** + * optional uint32 p_uint32 = 41; + * + * @return This builder for chaining. + */ + public Builder clearPUint32() { + bitField0_ = (bitField0_ & ~0x00200000); + pUint32_ = 0; + onChanged(); + return this; + } + + private int pFixed32_; + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return Whether the pFixed32 field is set. + */ + @java.lang.Override + public boolean hasPFixed32() { + return ((bitField0_ & 0x00400000) != 0); + } + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return The pFixed32. + */ + @java.lang.Override + public int getPFixed32() { + return pFixed32_; + } + + /** + * optional fixed32 p_fixed32 = 42; + * + * @param value The pFixed32 to set. + * @return This builder for chaining. + */ + public Builder setPFixed32(int value) { + + pFixed32_ = value; + bitField0_ |= 0x00400000; + onChanged(); + return this; + } + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return This builder for chaining. + */ + public Builder clearPFixed32() { + bitField0_ = (bitField0_ & ~0x00400000); + pFixed32_ = 0; + onChanged(); + return this; + } + + private long pInt64_; + + /** + * optional int64 p_int64 = 43; + * + * @return Whether the pInt64 field is set. + */ + @java.lang.Override + public boolean hasPInt64() { + return ((bitField0_ & 0x00800000) != 0); + } + + /** + * optional int64 p_int64 = 43; + * + * @return The pInt64. + */ + @java.lang.Override + public long getPInt64() { + return pInt64_; + } + + /** + * optional int64 p_int64 = 43; + * + * @param value The pInt64 to set. + * @return This builder for chaining. + */ + public Builder setPInt64(long value) { + + pInt64_ = value; + bitField0_ |= 0x00800000; + onChanged(); + return this; + } + + /** + * optional int64 p_int64 = 43; + * + * @return This builder for chaining. + */ + public Builder clearPInt64() { + bitField0_ = (bitField0_ & ~0x00800000); + pInt64_ = 0L; + onChanged(); + return this; + } + + private long pSint64_; + + /** + * optional sint64 p_sint64 = 44; + * + * @return Whether the pSint64 field is set. + */ + @java.lang.Override + public boolean hasPSint64() { + return ((bitField0_ & 0x01000000) != 0); + } + + /** + * optional sint64 p_sint64 = 44; + * + * @return The pSint64. + */ + @java.lang.Override + public long getPSint64() { + return pSint64_; + } + + /** + * optional sint64 p_sint64 = 44; + * + * @param value The pSint64 to set. + * @return This builder for chaining. + */ + public Builder setPSint64(long value) { + + pSint64_ = value; + bitField0_ |= 0x01000000; + onChanged(); + return this; + } + + /** + * optional sint64 p_sint64 = 44; + * + * @return This builder for chaining. + */ + public Builder clearPSint64() { + bitField0_ = (bitField0_ & ~0x01000000); + pSint64_ = 0L; + onChanged(); + return this; + } + + private long pSfixed64_; + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return Whether the pSfixed64 field is set. + */ + @java.lang.Override + public boolean hasPSfixed64() { + return ((bitField0_ & 0x02000000) != 0); + } + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return The pSfixed64. + */ + @java.lang.Override + public long getPSfixed64() { + return pSfixed64_; + } + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @param value The pSfixed64 to set. + * @return This builder for chaining. + */ + public Builder setPSfixed64(long value) { + + pSfixed64_ = value; + bitField0_ |= 0x02000000; + onChanged(); + return this; + } + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return This builder for chaining. + */ + public Builder clearPSfixed64() { + bitField0_ = (bitField0_ & ~0x02000000); + pSfixed64_ = 0L; + onChanged(); + return this; + } + + private long pUint64_; + + /** + * optional uint64 p_uint64 = 46; + * + * @return Whether the pUint64 field is set. + */ + @java.lang.Override + public boolean hasPUint64() { + return ((bitField0_ & 0x04000000) != 0); + } + + /** + * optional uint64 p_uint64 = 46; + * + * @return The pUint64. + */ + @java.lang.Override + public long getPUint64() { + return pUint64_; + } + + /** + * optional uint64 p_uint64 = 46; + * + * @param value The pUint64 to set. + * @return This builder for chaining. + */ + public Builder setPUint64(long value) { + + pUint64_ = value; + bitField0_ |= 0x04000000; + onChanged(); + return this; + } + + /** + * optional uint64 p_uint64 = 46; + * + * @return This builder for chaining. + */ + public Builder clearPUint64() { + bitField0_ = (bitField0_ & ~0x04000000); + pUint64_ = 0L; + onChanged(); + return this; + } + + private long pFixed64_; + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return Whether the pFixed64 field is set. + */ + @java.lang.Override + public boolean hasPFixed64() { + return ((bitField0_ & 0x08000000) != 0); + } + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return The pFixed64. + */ + @java.lang.Override + public long getPFixed64() { + return pFixed64_; + } + + /** + * optional fixed64 p_fixed64 = 47; + * + * @param value The pFixed64 to set. + * @return This builder for chaining. + */ + public Builder setPFixed64(long value) { + + pFixed64_ = value; + bitField0_ |= 0x08000000; + onChanged(); + return this; + } + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return This builder for chaining. + */ + public Builder clearPFixed64() { + bitField0_ = (bitField0_ & ~0x08000000); + pFixed64_ = 0L; + onChanged(); + return this; + } + + private float pFloat_; + + /** + * optional float p_float = 48; + * + * @return Whether the pFloat field is set. + */ + @java.lang.Override + public boolean hasPFloat() { + return ((bitField0_ & 0x10000000) != 0); + } + + /** + * optional float p_float = 48; + * + * @return The pFloat. + */ + @java.lang.Override + public float getPFloat() { + return pFloat_; + } + + /** + * optional float p_float = 48; + * + * @param value The pFloat to set. + * @return This builder for chaining. + */ + public Builder setPFloat(float value) { + + pFloat_ = value; + bitField0_ |= 0x10000000; + onChanged(); + return this; + } + + /** + * optional float p_float = 48; + * + * @return This builder for chaining. + */ + public Builder clearPFloat() { + bitField0_ = (bitField0_ & ~0x10000000); + pFloat_ = 0F; + onChanged(); + return this; + } + + private double pDouble_; /** * optional double p_double = 19; @@ -2517,7 +3519,7 @@ public Builder clearPInt32() { */ @java.lang.Override public boolean hasPDouble() { - return ((bitField0_ & 0x00080000) != 0); + return ((bitField0_ & 0x20000000) != 0); } /** @@ -2539,7 +3541,7 @@ public double getPDouble() { public Builder setPDouble(double value) { pDouble_ = value; - bitField0_ |= 0x00080000; + bitField0_ |= 0x20000000; onChanged(); return this; } @@ -2550,7 +3552,7 @@ public Builder setPDouble(double value) { * @return This builder for chaining. */ public Builder clearPDouble() { - bitField0_ = (bitField0_ & ~0x00080000); + bitField0_ = (bitField0_ & ~0x20000000); pDouble_ = 0D; onChanged(); return this; @@ -2565,7 +3567,7 @@ public Builder clearPDouble() { */ @java.lang.Override public boolean hasPBool() { - return ((bitField0_ & 0x00100000) != 0); + return ((bitField0_ & 0x40000000) != 0); } /** @@ -2587,7 +3589,7 @@ public boolean getPBool() { public Builder setPBool(boolean value) { pBool_ = value; - bitField0_ |= 0x00100000; + bitField0_ |= 0x40000000; onChanged(); return this; } @@ -2598,7 +3600,7 @@ public Builder setPBool(boolean value) { * @return This builder for chaining. */ public Builder clearPBool() { - bitField0_ = (bitField0_ & ~0x00100000); + bitField0_ = (bitField0_ & ~0x40000000); pBool_ = false; onChanged(); return this; @@ -2613,7 +3615,7 @@ public Builder clearPBool() { */ @java.lang.Override public boolean hasPKingdom() { - return ((bitField0_ & 0x00200000) != 0); + return ((bitField0_ & 0x80000000) != 0); } /** @@ -2634,7 +3636,7 @@ public int getPKingdomValue() { */ public Builder setPKingdomValue(int value) { pKingdom_ = value; - bitField0_ |= 0x00200000; + bitField0_ |= 0x80000000; onChanged(); return this; } @@ -2663,7 +3665,7 @@ public Builder setPKingdom(com.google.showcase.v1beta1.ComplianceData.LifeKingdo if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00200000; + bitField0_ |= 0x80000000; pKingdom_ = value.getNumber(); onChanged(); return this; @@ -2675,7 +3677,7 @@ public Builder setPKingdom(com.google.showcase.v1beta1.ComplianceData.LifeKingdo * @return This builder for chaining. */ public Builder clearPKingdom() { - bitField0_ = (bitField0_ & ~0x00200000); + bitField0_ = (bitField0_ & ~0x80000000); pKingdom_ = 0; onChanged(); return this; @@ -2694,7 +3696,7 @@ public Builder clearPKingdom() { * @return Whether the pChild field is set. */ public boolean hasPChild() { - return ((bitField0_ & 0x00400000) != 0); + return ((bitField1_ & 0x00000001) != 0); } /** @@ -2722,7 +3724,7 @@ public Builder setPChild(com.google.showcase.v1beta1.ComplianceDataChild value) } else { pChildBuilder_.setMessage(value); } - bitField0_ |= 0x00400000; + bitField1_ |= 0x00000001; onChanged(); return this; } @@ -2735,7 +3737,7 @@ public Builder setPChild( } else { pChildBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00400000; + bitField1_ |= 0x00000001; onChanged(); return this; } @@ -2743,7 +3745,7 @@ public Builder setPChild( /** optional .google.showcase.v1beta1.ComplianceDataChild p_child = 21; */ public Builder mergePChild(com.google.showcase.v1beta1.ComplianceDataChild value) { if (pChildBuilder_ == null) { - if (((bitField0_ & 0x00400000) != 0) + if (((bitField1_ & 0x00000001) != 0) && pChild_ != null && pChild_ != com.google.showcase.v1beta1.ComplianceDataChild.getDefaultInstance()) { getPChildBuilder().mergeFrom(value); @@ -2754,7 +3756,7 @@ public Builder mergePChild(com.google.showcase.v1beta1.ComplianceDataChild value pChildBuilder_.mergeFrom(value); } if (pChild_ != null) { - bitField0_ |= 0x00400000; + bitField1_ |= 0x00000001; onChanged(); } return this; @@ -2762,7 +3764,7 @@ public Builder mergePChild(com.google.showcase.v1beta1.ComplianceDataChild value /** optional .google.showcase.v1beta1.ComplianceDataChild p_child = 21; */ public Builder clearPChild() { - bitField0_ = (bitField0_ & ~0x00400000); + bitField1_ = (bitField1_ & ~0x00000001); pChild_ = null; if (pChildBuilder_ != null) { pChildBuilder_.dispose(); @@ -2774,7 +3776,7 @@ public Builder clearPChild() { /** optional .google.showcase.v1beta1.ComplianceDataChild p_child = 21; */ public com.google.showcase.v1beta1.ComplianceDataChild.Builder getPChildBuilder() { - bitField0_ |= 0x00400000; + bitField1_ |= 0x00000001; onChanged(); return internalGetPChildFieldBuilder().getBuilder(); } diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceDataOrBuilder.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceDataOrBuilder.java index e60610462c..e419c88349 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceDataOrBuilder.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceDataOrBuilder.java @@ -204,6 +204,146 @@ public interface ComplianceDataOrBuilder */ int getPInt32(); + /** + * optional sint32 p_sint32 = 39; + * + * @return Whether the pSint32 field is set. + */ + boolean hasPSint32(); + + /** + * optional sint32 p_sint32 = 39; + * + * @return The pSint32. + */ + int getPSint32(); + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return Whether the pSfixed32 field is set. + */ + boolean hasPSfixed32(); + + /** + * optional sfixed32 p_sfixed32 = 40; + * + * @return The pSfixed32. + */ + int getPSfixed32(); + + /** + * optional uint32 p_uint32 = 41; + * + * @return Whether the pUint32 field is set. + */ + boolean hasPUint32(); + + /** + * optional uint32 p_uint32 = 41; + * + * @return The pUint32. + */ + int getPUint32(); + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return Whether the pFixed32 field is set. + */ + boolean hasPFixed32(); + + /** + * optional fixed32 p_fixed32 = 42; + * + * @return The pFixed32. + */ + int getPFixed32(); + + /** + * optional int64 p_int64 = 43; + * + * @return Whether the pInt64 field is set. + */ + boolean hasPInt64(); + + /** + * optional int64 p_int64 = 43; + * + * @return The pInt64. + */ + long getPInt64(); + + /** + * optional sint64 p_sint64 = 44; + * + * @return Whether the pSint64 field is set. + */ + boolean hasPSint64(); + + /** + * optional sint64 p_sint64 = 44; + * + * @return The pSint64. + */ + long getPSint64(); + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return Whether the pSfixed64 field is set. + */ + boolean hasPSfixed64(); + + /** + * optional sfixed64 p_sfixed64 = 45; + * + * @return The pSfixed64. + */ + long getPSfixed64(); + + /** + * optional uint64 p_uint64 = 46; + * + * @return Whether the pUint64 field is set. + */ + boolean hasPUint64(); + + /** + * optional uint64 p_uint64 = 46; + * + * @return The pUint64. + */ + long getPUint64(); + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return Whether the pFixed64 field is set. + */ + boolean hasPFixed64(); + + /** + * optional fixed64 p_fixed64 = 47; + * + * @return The pFixed64. + */ + long getPFixed64(); + + /** + * optional float p_float = 48; + * + * @return Whether the pFloat field is set. + */ + boolean hasPFloat(); + + /** + * optional float p_float = 48; + * + * @return The pFloat. + */ + float getPFloat(); + /** * optional double p_double = 19; * diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceOuterClass.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceOuterClass.java index 3bbb7794aa..86fc02ea43 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceOuterClass.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/ComplianceOuterClass.java @@ -113,7 +113,7 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\017ComplianceGroup\022\014\n" + "\004name\030\001 \001(\t\022\014\n" + "\004rpcs\030\002 \003(\t\0228\n" - + "\010requests\030\003 \003(\0132&.google.showcase.v1beta1.RepeatRequest\"\340\006\n" + + "\010requests\030\003 \003(\0132&.google.showcase.v1beta1.RepeatRequest\"\320\t\n" + "\016ComplianceData\022\020\n" + "\010f_string\030\001 \001(\t\022\017\n" + "\007f_int32\030\002 \001(\005\022\020\n" @@ -138,12 +138,25 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\007f_child\030\020 \001(\0132,.google.showcase.v1beta1.ComplianceDataChild\022\025\n" + "\010p_string\030\021 \001(\tH\000\210\001\001\022\024\n" + "\007p_int32\030\022 \001(\005H\001\210\001\001\022\025\n" - + "\010p_double\030\023 \001(\001H\002\210\001\001\022\023\n" - + "\006p_bool\030\024 \001(\010H\003\210\001\001\022K\n" - + "\tp_kingdom\030\027 \001(\01623.google.showca" - + "se.v1beta1.ComplianceData.LifeKingdomH\004\210\001\001\022B\n" + + "\010p_sint32\030\' \001(\021H\002\210\001\001\022\027\n\n" + + "p_sfixed32\030( \001(\017H\003\210\001\001\022\025\n" + + "\010p_uint32\030) \001(\r" + + "H\004\210\001\001\022\026\n" + + "\tp_fixed32\030* \001(\007H\005\210\001\001\022\024\n" + + "\007p_int64\030+ \001(\003H\006\210\001\001\022\025\n" + + "\010p_sint64\030, \001(\022H\007\210\001\001\022\027\n\n" + + "p_sfixed64\030- \001(\020H\010\210\001\001\022\025\n" + + "\010p_uint64\030. \001(\004H\t\210\001\001\022\026\n" + + "\tp_fixed64\030/ \001(\006H\n" + + "\210\001\001\022\024\n" + + "\007p_float\0300 \001(\002H\013\210\001\001\022\025\n" + + "\010p_double\030\023 \001(\001H\014\210\001\001\022\023\n" + + "\006p_bool\030\024 \001(\010H\r" + + "\210\001\001\022K\n" + + "\tp_kingdom\030\027" + + " \001(\01623.google.showcase.v1beta1.ComplianceData.LifeKingdomH\016\210\001\001\022B\n" + "\007p_child\030\025" - + " \001(\0132,.google.showcase.v1beta1.ComplianceDataChildH\005\210\001\001\"\203\001\n" + + " \001(\0132,.google.showcase.v1beta1.ComplianceDataChildH\017\210\001\001\"\203\001\n" + "\013LifeKingdom\022\034\n" + "\030LIFE_KINGDOM_UNSPECIFIED\020\000\022\022\n" + "\016ARCHAEBACTERIA\020\001\022\016\n\n" @@ -154,6 +167,17 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\010ANIMALIA\020\006B\013\n" + "\t_p_stringB\n\n" + "\010_p_int32B\013\n" + + "\t_p_sint32B\r\n" + + "\013_p_sfixed32B\013\n" + + "\t_p_uint32B\014\n\n" + + "_p_fixed32B\n" + + "\n" + + "\010_p_int64B\013\n" + + "\t_p_sint64B\r\n" + + "\013_p_sfixed64B\013\n" + + "\t_p_uint64B\014\n\n" + + "_p_fixed64B\n\n" + + "\010_p_floatB\013\n" + "\t_p_doubleB\t\n" + "\007_p_boolB\014\n\n" + "_p_kingdomB\n\n" @@ -195,43 +219,44 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { + "\tAUSTRALIA\020\004\022\n\n" + "\006EUROPE\020\0052\330\r\n\n" + "Compliance\022\202\001\n" - + "\016RepeatDataBody\022&.google.showcase.v1beta1.RepeatReques" - + "t\032\'.google.showcase.v1beta1.RepeatResponse\"\037\202\323\344\223\002\031\"\024/v1beta1/repeat:body:\001*\022\215\001\n" - + "\022RepeatDataBodyInfo\022&.google.showcase.v1b" - + "eta1.RepeatRequest\032\'.google.showcase.v1beta1.RepeatResponse\"&\202\323\344\223\002" + + "\016RepeatDataBody\022&.google.showcase.v1beta1.Repe" + + "atRequest\032\'.google.showcase.v1beta1.Repe" + + "atResponse\"\037\202\323\344\223\002\031\"\024/v1beta1/repeat:body:\001*\022\215\001\n" + + "\022RepeatDataBodyInfo\022&.google.show" + + "case.v1beta1.RepeatRequest\032\'.google.showcase.v1beta1.RepeatResponse\"&\202\323\344\223\002" + " \"\030/v1beta1/repeat:bodyinfo:\004info\022\201\001\n" - + "\017RepeatDataQuery\022&.google.showcase.v1beta1.RepeatRequest\032" - + "\'.google.showcase.v1beta1.RepeatResponse\"\035\202\323\344\223\002\027\022\025/v1beta1/repeat:query\022\331\001\n" - + "\024RepeatDataSimplePath\022&.google.showcase.v1bet" - + "a1.RepeatRequest\032\'.google.showcase.v1bet" - + "a1.RepeatResponse\"p\202\323\344\223\002j\022h/v1beta1/repe" - + "at/{info.f_string}/{info.f_int32}/{info." - + "f_double}/{info.f_bool}/{info.f_kingdom}:simplepath\022\323\002\n" - + "\026RepeatDataPathResource\022&.google.showcase.v1beta1.RepeatRequest\032\'" - + ".google.showcase.v1beta1.RepeatResponse\"" - + "\347\001\202\323\344\223\002\340\001\022h/v1beta1/repeat/{info.f_strin" - + "g=first/*}/{info.f_child.f_string=second/*}/bool/{info.f_bool}:pathresourceZt\022r/" - + "v1beta1/repeat/{info.f_child.f_string=fi" - + "rst/*}/{info.f_string=second/*}/bool/{info.f_bool}:childfirstpathresource\022\331\001\n" - + "\036RepeatDataPathTrailingResource\022&.google.sh" - + "owcase.v1beta1.RepeatRequest\032\'.google.sh" - + "owcase.v1beta1.RepeatResponse\"f\202\323\344\223\002`\022^/" - + "v1beta1/repeat/{info.f_string=first/*}/{" - + "info.f_child.f_string=second/**}:pathtrailingresource\022\210\001\n" - + "\021RepeatDataBodyPut\022&.google.showcase.v1beta1.RepeatRequest\032\'.go" - + "ogle.showcase.v1beta1.RepeatResponse\"\"\202\323\344\223\002\034\032\027/v1beta1/repeat:bodyput:\001*\022\214\001\n" - + "\023RepeatDataBodyPatch\022&.google.showcase.v1bet" - + "a1.RepeatRequest\032\'.google.showcase.v1bet" - + "a1.RepeatResponse\"$\202\323\344\223\002\0362\031/v1beta1/repeat:bodypatch:\001*\022x\n" - + "\007GetEnum\022$.google.show" - + "case.v1beta1.EnumRequest\032%.google.showcase.v1beta1.EnumResponse\"" + + "\017RepeatDataQuery\022&.google.showcase.v1beta1.Repeat" + + "Request\032\'.google.showcase.v1beta1.Repeat" + + "Response\"\035\202\323\344\223\002\027\022\025/v1beta1/repeat:query\022\331\001\n" + + "\024RepeatDataSimplePath\022&.google.showca" + + "se.v1beta1.RepeatRequest\032\'.google.showca" + + "se.v1beta1.RepeatResponse\"p\202\323\344\223\002j\022h/v1be" + + "ta1/repeat/{info.f_string}/{info.f_int32" + + "}/{info.f_double}/{info.f_bool}/{info.f_kingdom}:simplepath\022\323\002\n" + + "\026RepeatDataPathResource\022&.google.showcase.v1beta1.RepeatR" + + "equest\032\'.google.showcase.v1beta1.RepeatR" + + "esponse\"\347\001\202\323\344\223\002\340\001\022h/v1beta1/repeat/{info" + + ".f_string=first/*}/{info.f_child.f_string=second/*}/bool/{info.f_bool}:pathresou" + + "rceZt\022r/v1beta1/repeat/{info.f_child.f_string=first/*}/{info.f_string=second/*}/" + + "bool/{info.f_bool}:childfirstpathresource\022\331\001\n" + + "\036RepeatDataPathTrailingResource\022&.google.showcase.v1beta1.RepeatRequest\032\'.g" + + "oogle.showcase.v1beta1.RepeatResponse\"f\202" + + "\323\344\223\002`\022^/v1beta1/repeat/{info.f_string=fi" + + "rst/*}/{info.f_child.f_string=second/**}:pathtrailingresource\022\210\001\n" + + "\021RepeatDataBodyPut\022&.google.showcase.v1beta1.RepeatRequ" + + "est\032\'.google.showcase.v1beta1.RepeatResp" + + "onse\"\"\202\323\344\223\002\034\032\027/v1beta1/repeat:bodyput:\001*\022\214\001\n" + + "\023RepeatDataBodyPatch\022&.google.showcase.v1beta1.RepeatRequest\032\'.google.showca" + + "se.v1beta1.RepeatResponse\"$\202\323\344\223\002\0362\031/v1beta1/repeat:bodypatch:\001*\022x\n" + + "\007GetEnum\022$.goo" + + "gle.showcase.v1beta1.EnumRequest\032%.google.showcase.v1beta1.EnumResponse\"" + " \202\323\344\223\002\032\022\030/v1beta1/compliance/enum\022|\n\n" - + "VerifyEnum\022%.google" - + ".showcase.v1beta1.EnumResponse\032%.google.showcase.v1beta1.EnumResponse\"" - + " \202\323\344\223\002\032\"\030/" - + "v1beta1/compliance/enum\032\021\312A\016localhost:7469Bq\n" - + "\033com.google.showcase.v1beta1P\001Z4github.com/googleapis/gapic-showcase/server" - + "/genproto\352\002\031Google::Showcase::V1beta1b\006proto3" + + "VerifyEnum\022" + + "%.google.showcase.v1beta1.EnumResponse\032%.google.showcase.v1beta1.EnumResponse\"" + + " \202" + + "\323\344\223\002\032\"\030/v1beta1/compliance/enum\032\021\312A\016localhost:7469Bq\n" + + "\033com.google.showcase.v1beta1P\001Z4github.com/googleapis/gapic-showcas" + + "e/server/genproto\352\002\031Google::Showcase::V1beta1b\006proto3" }; descriptor = com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom( @@ -306,6 +331,16 @@ public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() { "FChild", "PString", "PInt32", + "PSint32", + "PSfixed32", + "PUint32", + "PFixed32", + "PInt64", + "PSint64", + "PSfixed64", + "PUint64", + "PFixed64", + "PFloat", "PDouble", "PBool", "PKingdom", diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateSequenceRequest.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateSequenceRequest.java index fb0d757e44..c4b45de9fc 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateSequenceRequest.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateSequenceRequest.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.CreateSequenceRequest} */ +/** + * + * + *
              + * Request message for creating a sequence of unary calls
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.CreateSequenceRequest} + */ @com.google.protobuf.Generated public final class CreateSequenceRequest extends com.google.protobuf.GeneratedMessage implements @@ -259,7 +267,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.CreateSequenceRequest} */ + /** + * + * + *
              +   * Request message for creating a sequence of unary calls
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.CreateSequenceRequest} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.CreateSequenceRequest) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateStreamingSequenceRequest.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateStreamingSequenceRequest.java index 43cc59a1fa..32189bf61c 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateStreamingSequenceRequest.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/CreateStreamingSequenceRequest.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.CreateStreamingSequenceRequest} */ +/** + * + * + *
              + * Request message for the sequences of responses to be sent in a server streaming call
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.CreateStreamingSequenceRequest} + */ @com.google.protobuf.Generated public final class CreateStreamingSequenceRequest extends com.google.protobuf.GeneratedMessage implements @@ -260,7 +268,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.CreateStreamingSequenceRequest} */ + /** + * + * + *
              +   * Request message for the sequences of responses to be sent in a server streaming call
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.CreateStreamingSequenceRequest} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.CreateStreamingSequenceRequest) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/Sequence.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/Sequence.java index c0f7c3095c..bf83ee3e44 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/Sequence.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/Sequence.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.Sequence} */ +/** + * + * + *
              + * A sequence of responses to be returned in order for each unary call attempt
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.Sequence} + */ @com.google.protobuf.Generated public final class Sequence extends com.google.protobuf.GeneratedMessage implements @@ -1370,7 +1378,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.Sequence} */ + /** + * + * + *
              +   * A sequence of responses to be returned in order for each unary call attempt
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.Sequence} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.Sequence) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceReport.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceReport.java index 321949e3ac..57c070e594 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceReport.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/SequenceReport.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.SequenceReport} */ +/** + * + * + *
              + * A report of the results of a sequence of unary responses
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.SequenceReport} + */ @com.google.protobuf.Generated public final class SequenceReport extends com.google.protobuf.GeneratedMessage implements @@ -2156,7 +2164,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.SequenceReport} */ + /** + * + * + *
              +   * A report of the results of a sequence of unary responses
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.SequenceReport} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.SequenceReport) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequence.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequence.java index 2aceeee676..06dd3a0bbd 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequence.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequence.java @@ -20,7 +20,16 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.StreamingSequence} */ +/** + * + * + *
              + * A sequence of responses to be returned in order at the delay specified
              + * as part of the server streaming call
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.StreamingSequence} + */ @com.google.protobuf.Generated public final class StreamingSequence extends com.google.protobuf.GeneratedMessage implements @@ -147,7 +156,7 @@ public interface ResponseOrBuilder * * *
              -     * The index that the status should be sent
              +     * The index that the status should be sent at
                    * 
              * * int32 response_index = 3; @@ -310,7 +319,7 @@ public com.google.protobuf.DurationOrBuilder getDelayOrBuilder() { * * *
              -     * The index that the status should be sent
              +     * The index that the status should be sent at
                    * 
              * * int32 response_index = 3; @@ -1089,7 +1098,7 @@ public com.google.protobuf.DurationOrBuilder getDelayOrBuilder() { * * *
              -       * The index that the status should be sent
              +       * The index that the status should be sent at
                      * 
              * * int32 response_index = 3; @@ -1105,7 +1114,7 @@ public int getResponseIndex() { * * *
              -       * The index that the status should be sent
              +       * The index that the status should be sent at
                      * 
              * * int32 response_index = 3; @@ -1125,7 +1134,7 @@ public Builder setResponseIndex(int value) { * * *
              -       * The index that the status should be sent
              +       * The index that the status should be sent at
                      * 
              * * int32 response_index = 3; @@ -1197,6 +1206,12 @@ public com.google.showcase.v1beta1.StreamingSequence.Response getDefaultInstance private volatile java.lang.Object name_ = ""; /** + * + * + *
              +   * The name of the streaming sequence.
              +   * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The name. @@ -1215,6 +1230,12 @@ public java.lang.String getName() { } /** + * + * + *
              +   * The name of the streaming sequence.
              +   * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The bytes for name. @@ -1241,7 +1262,8 @@ public com.google.protobuf.ByteString getNameBytes() { * * *
              -   * The Content that the stream will send
              +   * The content that the stream will send
              +   * this was specified when the sequence was created
                  * 
              * * string content = 2; @@ -1265,7 +1287,8 @@ public java.lang.String getContent() { * * *
              -   * The Content that the stream will send
              +   * The content that the stream will send
              +   * this was specified when the sequence was created
                  * 
              * * string content = 2; @@ -1547,7 +1570,16 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.StreamingSequence} */ + /** + * + * + *
              +   * A sequence of responses to be returned in order at the delay specified
              +   * as part of the server streaming call
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.StreamingSequence} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.StreamingSequence) @@ -1767,6 +1799,12 @@ public Builder mergeFrom( private java.lang.Object name_ = ""; /** + * + * + *
              +     * The name of the streaming sequence.
              +     * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The name. @@ -1784,6 +1822,12 @@ public java.lang.String getName() { } /** + * + * + *
              +     * The name of the streaming sequence.
              +     * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The bytes for name. @@ -1801,6 +1845,12 @@ public com.google.protobuf.ByteString getNameBytes() { } /** + * + * + *
              +     * The name of the streaming sequence.
              +     * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @param value The name to set. @@ -1817,6 +1867,12 @@ public Builder setName(java.lang.String value) { } /** + * + * + *
              +     * The name of the streaming sequence.
              +     * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return This builder for chaining. @@ -1829,6 +1885,12 @@ public Builder clearName() { } /** + * + * + *
              +     * The name of the streaming sequence.
              +     * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @param value The bytes for name to set. @@ -1851,7 +1913,8 @@ public Builder setNameBytes(com.google.protobuf.ByteString value) { * * *
              -     * The Content that the stream will send
              +     * The content that the stream will send
              +     * this was specified when the sequence was created
                    * 
              * * string content = 2; @@ -1874,7 +1937,8 @@ public java.lang.String getContent() { * * *
              -     * The Content that the stream will send
              +     * The content that the stream will send
              +     * this was specified when the sequence was created
                    * 
              * * string content = 2; @@ -1897,7 +1961,8 @@ public com.google.protobuf.ByteString getContentBytes() { * * *
              -     * The Content that the stream will send
              +     * The content that the stream will send
              +     * this was specified when the sequence was created
                    * 
              * * string content = 2; @@ -1919,7 +1984,8 @@ public Builder setContent(java.lang.String value) { * * *
              -     * The Content that the stream will send
              +     * The content that the stream will send
              +     * this was specified when the sequence was created
                    * 
              * * string content = 2; @@ -1937,7 +2003,8 @@ public Builder clearContent() { * * *
              -     * The Content that the stream will send
              +     * The content that the stream will send
              +     * this was specified when the sequence was created
                    * 
              * * string content = 2; diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceOrBuilder.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceOrBuilder.java index f743a4087c..2af9e24c7f 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceOrBuilder.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceOrBuilder.java @@ -27,6 +27,12 @@ public interface StreamingSequenceOrBuilder com.google.protobuf.MessageOrBuilder { /** + * + * + *
              +   * The name of the streaming sequence.
              +   * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The name. @@ -34,6 +40,12 @@ public interface StreamingSequenceOrBuilder java.lang.String getName(); /** + * + * + *
              +   * The name of the streaming sequence.
              +   * 
              + * * string name = 1 [(.google.api.field_behavior) = OUTPUT_ONLY]; * * @return The bytes for name. @@ -44,7 +56,8 @@ public interface StreamingSequenceOrBuilder * * *
              -   * The Content that the stream will send
              +   * The content that the stream will send
              +   * this was specified when the sequence was created
                  * 
              * * string content = 2; @@ -57,7 +70,8 @@ public interface StreamingSequenceOrBuilder * * *
              -   * The Content that the stream will send
              +   * The content that the stream will send
              +   * this was specified when the sequence was created
                  * 
              * * string content = 2; diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceReport.java b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceReport.java index 4d14fafae1..db187a558e 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceReport.java +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/java/com/google/showcase/v1beta1/StreamingSequenceReport.java @@ -20,7 +20,15 @@ package com.google.showcase.v1beta1; -/** Protobuf type {@code google.showcase.v1beta1.StreamingSequenceReport} */ +/** + * + * + *
              + * A report of the results of a streaming sequence.
              + * 
              + * + * Protobuf type {@code google.showcase.v1beta1.StreamingSequenceReport} + */ @com.google.protobuf.Generated public final class StreamingSequenceReport extends com.google.protobuf.GeneratedMessage implements @@ -2163,7 +2171,15 @@ protected Builder newBuilderForType(com.google.protobuf.GeneratedMessage.Builder return builder; } - /** Protobuf type {@code google.showcase.v1beta1.StreamingSequenceReport} */ + /** + * + * + *
              +   * A report of the results of a streaming sequence.
              +   * 
              + * + * Protobuf type {@code google.showcase.v1beta1.StreamingSequenceReport} + */ public static final class Builder extends com.google.protobuf.GeneratedMessage.Builder implements // @@protoc_insertion_point(builder_implements:google.showcase.v1beta1.StreamingSequenceReport) diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/compliance.proto b/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/compliance.proto index 4e8cba6b25..48c71bbb9b 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/compliance.proto +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/compliance.proto @@ -216,9 +216,37 @@ message ComplianceData { // optional fields optional string p_string = 17; + optional int32 p_int32 = 18; + optional sint32 p_sint32 = 39; + optional sfixed32 p_sfixed32 = 40; + + optional uint32 p_uint32 = 41; + optional fixed32 p_fixed32 = 42; + + optional int64 p_int64 = 43; + optional sint64 p_sint64 = 44; + optional sfixed64 p_sfixed64 = 45; + + optional uint64 p_uint64 = 46; + optional fixed64 p_fixed64 = 47; + + optional float p_float = 48; optional double p_double = 19; + optional bool p_bool = 20; + + // The proto compiler seems to misgenerate optional bytes fields. + // The generated code looks like: + // ``` + // if cmd.Flags().Changed("info.p_bytes") { + // RepeatDataBodyInfoInput.Info.PBytes = &repeatDataBodyInfoInputInfoPBytes + // } + // ``` + // The dereference of `&repeatDataBodyInfoInputInfoPBytes` is incorrect since + // `repeatDataBodyInfoInputInfoPBytes` is already a `byte[]` + // optional bytes p_bytes = 49; + optional LifeKingdom p_kingdom = 23; optional ComplianceDataChild p_child = 21; } @@ -254,7 +282,6 @@ enum Continent { EUROPE = 5; } - message EnumRequest { // Whether the client is requesting a new, unknown enum value or a known enum value already declared in this proto file. bool unknown_enum = 1; diff --git a/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/sequence.proto b/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/sequence.proto index a4e3ee28f1..c6fd6a3f38 100644 --- a/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/sequence.proto +++ b/java-showcase/proto-gapic-showcase-v1beta1/src/main/proto/schema/google/showcase/v1beta1/sequence.proto @@ -30,12 +30,14 @@ option java_package = "com.google.showcase.v1beta1"; option java_multiple_files = true; option ruby_package = "Google::Showcase::V1beta1"; +// A service that enables testing of unary and server streaming calls +// by specifying a specific, predictable sequence of responses from the service service SequenceService { // This service is meant to only run locally on the port 7469 (keypad digits // for "show"). option (google.api.default_host) = "localhost:7469"; - // Creates a sequence. + // Create a sequence of responses to be returned as unary calls rpc CreateSequence(CreateSequenceRequest) returns (Sequence) { option (google.api.http) = { post: "/v1beta1/sequences" @@ -44,7 +46,7 @@ service SequenceService { option (google.api.method_signature) = "sequence"; }; - // Creates a sequence. + // Creates a sequence of responses to be returned in a server streaming call rpc CreateStreamingSequence(CreateStreamingSequenceRequest) returns (StreamingSequence) { option (google.api.http) = { post: "/v1beta1/streamingSequences" @@ -53,7 +55,8 @@ service SequenceService { option (google.api.method_signature) = "streaming_sequence"; }; - // Retrieves a sequence. + // Retrieves a sequence report which can be used to retrieve information about a + // sequence of unary calls. rpc GetSequenceReport(GetSequenceReportRequest) returns (SequenceReport) { option (google.api.http) = { get: "/v1beta1/{name=sequences/*/sequenceReport}" @@ -61,7 +64,8 @@ service SequenceService { option (google.api.method_signature) = "name"; }; - // Retrieves a sequence. + // Retrieves a sequence report which can be used to retrieve information + // about a sequences of responses in a server streaming call. rpc GetStreamingSequenceReport(GetStreamingSequenceReportRequest) returns (StreamingSequenceReport) { option (google.api.http) = { get: "/v1beta1/{name=streamingSequences/*/streamingSequenceReport}" @@ -69,7 +73,7 @@ service SequenceService { option (google.api.method_signature) = "name"; }; - // Attempts a sequence. + // Attempts a sequence of unary responses. rpc AttemptSequence(AttemptSequenceRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/v1beta1/{name=sequences/*}" @@ -78,7 +82,8 @@ service SequenceService { option (google.api.method_signature) = "name"; }; - // Attempts a streaming sequence. + // Attempts a server streaming call with a sequence of responses + // Can be used to test retries and stream resumption logic // May not function as expected in HTTP mode due to when http statuses are sent // See https://github.com/googleapis/gapic-showcase/issues/1377 for more details rpc AttemptStreamingSequence(AttemptStreamingSequenceRequest) returns (stream AttemptStreamingSequenceResponse) { @@ -90,6 +95,7 @@ service SequenceService { }; } +// A sequence of responses to be returned in order for each unary call attempt message Sequence { option (google.api.resource) = { type: "showcase.googleapis.com/Sequence" @@ -112,15 +118,19 @@ message Sequence { repeated Response responses = 2; } +// A sequence of responses to be returned in order at the delay specified +// as part of the server streaming call message StreamingSequence { option (google.api.resource) = { type: "showcase.googleapis.com/StreamingSequence" pattern: "streamingSequences/{streaming_sequence}" }; + // The name of the streaming sequence. string name = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; - // The Content that the stream will send + // The content that the stream will send + // this was specified when the sequence was created string content = 2; // A server response to an RPC Attempt in a sequence. @@ -131,7 +141,7 @@ message StreamingSequence { // The amount of time to delay sending the response. google.protobuf.Duration delay = 2; - // The index that the status should be sent + // The index that the status should be sent at int32 response_index = 3; } @@ -140,7 +150,7 @@ message StreamingSequence { repeated Response responses = 3; } - +// A report of the results of a streaming sequence. message StreamingSequenceReport { option (google.api.resource) = { type: "showcase.googleapis.com/StreamingSequenceReport" @@ -174,6 +184,7 @@ message StreamingSequenceReport { repeated Attempt attempts = 2; } +// A report of the results of a sequence of unary responses message SequenceReport { option (google.api.resource) = { type: "showcase.googleapis.com/SequenceReport" @@ -206,14 +217,17 @@ message SequenceReport { repeated Attempt attempts = 2; } +// Request message for creating a sequence of unary calls message CreateSequenceRequest { Sequence sequence = 1; } +// Request message for the sequences of responses to be sent in a server streaming call message CreateStreamingSequenceRequest { StreamingSequence streaming_sequence = 1; } +// Request message for the unary AttemptSequence method message AttemptSequenceRequest { string name = 1 [ (google.api.resource_reference).type = "showcase.googleapis.com/Sequence", @@ -222,6 +236,7 @@ message AttemptSequenceRequest { } +// Request message for the AttemptStreamingSequence method. message AttemptStreamingSequenceRequest { string name = 1 [ (google.api.resource_reference).type = "showcase.googleapis.com/StreamingSequence", @@ -236,7 +251,7 @@ message AttemptStreamingSequenceRequest { ]; } -// The response message for the Echo methods. +// The response message for the AttemptStreamingSequence method. message AttemptStreamingSequenceResponse { // The content specified in the request. string content = 1; @@ -257,4 +272,4 @@ message GetStreamingSequenceReportRequest { "showcase.googleapis.com/StreamingSequenceReport", (google.api.field_behavior) = REQUIRED ]; -} +} \ No newline at end of file diff --git a/scripts/update_golden.sh b/scripts/update_golden.sh index 923b24b9bc..ff6273830b 100755 --- a/scripts/update_golden.sh +++ b/scripts/update_golden.sh @@ -11,8 +11,8 @@ cd srcjar_unpacked UNPACK_DIR=$PWD unzip -q -c "../${RAW_SRCJAR}" temp-codegen.srcjar | jar x -mkdir -p ${BUILD_WORKSPACE_DIRECTORY}/test/integration/goldens/${API_NAME} -cd ${BUILD_WORKSPACE_DIRECTORY}/test/integration/goldens/${API_NAME} +mkdir -p ${BUILD_WORKSPACE_DIRECTORY}/sdk-platform-java/test/integration/goldens/${API_NAME} +cd ${BUILD_WORKSPACE_DIRECTORY}/sdk-platform-java/test/integration/goldens/${API_NAME} # clear out existing Java and JSON files. find . -name '*.java' -delete diff --git a/sdk-platform-java-config/pom.xml b/sdk-platform-java-config/pom.xml index 0216342c79..f47de4108c 100644 --- a/sdk-platform-java-config/pom.xml +++ b/sdk-platform-java-config/pom.xml @@ -4,7 +4,7 @@ com.google.cloud sdk-platform-java-config pom - 3.58.1-SNAPSHOT + 3.58.1-SNAPSHOT SDK Platform For Java Configurations Shared build configuration for Google Cloud Java libraries. diff --git a/settings.xml b/settings.xml index 5f2958623a..0dcdbd3a6a 100644 --- a/settings.xml +++ b/settings.xml @@ -1,32 +1,16 @@ - - - - - - - - - - - - - + + + + google-maven-central + GCS Maven Central mirror + https://maven-central.storage-download.googleapis.com/maven2/ + central + + diff --git a/test/integration/goldens/README.md b/test/integration/goldens/README.md index a797bdac4f..11c1b6c811 100644 --- a/test/integration/goldens/README.md +++ b/test/integration/goldens/README.md @@ -12,7 +12,7 @@ files. If they are not identical, then the integration test will fail. ```sh -bazelisk test //test/integration:redis +bazelisk test //sdk-platform-java/test/integration:redis ``` ## How To Update Goldens @@ -24,7 +24,7 @@ in `redis` folder. ```sh # In repository's root directory mvn clean install -DskipTests -bazelisk run //test/integration:update_redis +bazelisk run //sdk-platform-java/test/integration:update_redis ``` ## Adding new integration tests diff --git a/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonAddressesStub.java b/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonAddressesStub.java index 9bb9f6c181..545ecf7a36 100644 --- a/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonAddressesStub.java +++ b/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonAddressesStub.java @@ -34,6 +34,7 @@ import com.google.api.gax.rpc.OperationCallable; import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.pathtemplate.PathTemplate; import com.google.cloud.compute.v1small.AddressAggregatedList; import com.google.cloud.compute.v1small.AddressList; import com.google.cloud.compute.v1small.AggregatedListAddressesRequest; @@ -274,6 +275,15 @@ public class HttpJsonAddressesStub extends AddressesStub { private final HttpJsonRegionOperationsStub httpJsonOperationsStub; private final HttpJsonStubCallableFactory callableFactory; + private static final PathTemplate AGGREGATED_LIST_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}"); + private static final PathTemplate DELETE_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/regions/{region}/addresses/{address}"); + private static final PathTemplate INSERT_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/regions/{region}"); + private static final PathTemplate LIST_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/regions/{region}"); + public static final HttpJsonAddressesStub create(AddressesStubSettings settings) throws IOException { return new HttpJsonAddressesStub(settings, ClientContext.create(settings)); @@ -324,6 +334,13 @@ protected HttpJsonAddressesStub( builder.add("project", String.valueOf(request.getProject())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + return AGGREGATED_LIST_RESOURCE_NAME_TEMPLATE.instantiate( + resourceNameSegments); + }) .build(); HttpJsonCallSettings deleteTransportSettings = HttpJsonCallSettings.newBuilder() @@ -337,6 +354,14 @@ protected HttpJsonAddressesStub( builder.add("region", String.valueOf(request.getRegion())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("address", String.valueOf(request.getAddress())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + resourceNameSegments.put("region", String.valueOf(request.getRegion())); + return DELETE_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); HttpJsonCallSettings insertTransportSettings = HttpJsonCallSettings.newBuilder() @@ -349,6 +374,13 @@ protected HttpJsonAddressesStub( builder.add("region", String.valueOf(request.getRegion())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + resourceNameSegments.put("region", String.valueOf(request.getRegion())); + return INSERT_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); HttpJsonCallSettings listTransportSettings = HttpJsonCallSettings.newBuilder() @@ -361,6 +393,13 @@ protected HttpJsonAddressesStub( builder.add("region", String.valueOf(request.getRegion())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + resourceNameSegments.put("region", String.valueOf(request.getRegion())); + return LIST_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); this.aggregatedListCallable = diff --git a/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonRegionOperationsStub.java b/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonRegionOperationsStub.java index e9e339169f..d0ef4b45db 100644 --- a/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonRegionOperationsStub.java +++ b/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonRegionOperationsStub.java @@ -32,6 +32,7 @@ import com.google.api.gax.rpc.LongRunningClient; import com.google.api.gax.rpc.RequestParamsBuilder; import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.pathtemplate.PathTemplate; import com.google.cloud.compute.v1small.GetRegionOperationRequest; import com.google.cloud.compute.v1small.Operation; import com.google.cloud.compute.v1small.Operation.Status; @@ -153,6 +154,11 @@ public class HttpJsonRegionOperationsStub extends RegionOperationsStub { private final LongRunningClient longRunningClient; private final HttpJsonStubCallableFactory callableFactory; + private static final PathTemplate GET_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/{project}/regions/{region}/operations/{operation}"); + private static final PathTemplate WAIT_RESOURCE_NAME_TEMPLATE = + PathTemplate.create("projects/projects/{project}/regions/{region}/operations/{operation}"); + public static final HttpJsonRegionOperationsStub create(RegionOperationsStubSettings settings) throws IOException { return new HttpJsonRegionOperationsStub(settings, ClientContext.create(settings)); @@ -204,6 +210,14 @@ protected HttpJsonRegionOperationsStub( builder.add("region", String.valueOf(request.getRegion())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("operation", String.valueOf(request.getOperation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + resourceNameSegments.put("region", String.valueOf(request.getRegion())); + return GET_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); HttpJsonCallSettings waitTransportSettings = HttpJsonCallSettings.newBuilder() @@ -217,6 +231,14 @@ protected HttpJsonRegionOperationsStub( builder.add("region", String.valueOf(request.getRegion())); return builder.build(); }) + .setResourceNameExtractor( + request -> { + Map resourceNameSegments = new HashMap(); + resourceNameSegments.put("operation", String.valueOf(request.getOperation())); + resourceNameSegments.put("project", String.valueOf(request.getProject())); + resourceNameSegments.put("region", String.valueOf(request.getRegion())); + return WAIT_RESOURCE_NAME_TEMPLATE.instantiate(resourceNameSegments); + }) .build(); this.getCallable =