diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties index 919c0da00b..7215a51375 100644 --- a/gax-java/dependencies.properties +++ b/gax-java/dependencies.properties @@ -39,8 +39,8 @@ maven.com_google_api_grpc_proto_google_common_protos=com.google.api.grpc:proto-g maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-google-common-protos:2.63.2 maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.42.1 maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.42.1 -maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.47.0 -maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.47.0 +maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.51.0 +maven.io_opentelemetry_opentelemetry_context=io.opentelemetry:opentelemetry-context:1.51.0 maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1 maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1 maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1 @@ -91,3 +91,8 @@ maven.net_bytebuddy_byte_buddy=net.bytebuddy:byte-buddy:1.17.0 maven.org_objenesis_objenesis=org.objenesis:objenesis:2.6 maven.org_junit_jupiter_junit_jupiter_api=org.junit.jupiter:junit-jupiter-api:5.11.4 maven.org_junit_jupiter_junit_jupiter_params=org.junit.jupiter:junit-jupiter-params:5.11.4 +maven.io_opentelemetry_opentelemetry_sdk_testing=io.opentelemetry:opentelemetry-sdk-testing:1.51.0 +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 diff --git a/gax-java/gax/BUILD.bazel b/gax-java/gax/BUILD.bazel index 15ed36bcbd..6f60891329 100644 --- a/gax-java/gax/BUILD.bazel +++ b/gax-java/gax/BUILD.bazel @@ -44,6 +44,11 @@ _TEST_COMPILE_DEPS = [ "@net_bytebuddy_byte_buddy//jar", "@org_objenesis_objenesis//jar", "@com_googlecode_java_diff_utils_diffutils//jar", + "@io_opentelemetry_opentelemetry_sdk_testing//jar", + "@io_opentelemetry_opentelemetry_sdk//jar", + "@io_opentelemetry_opentelemetry_sdk_metrics//jar", + "@io_opentelemetry_opentelemetry_sdk_common//jar", + "@com_google_guava_guava_testlib//jar", ] java_library( diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml index 17fd8a65d0..78c65c4f42 100644 --- a/gax-java/gax/pom.xml +++ b/gax-java/gax/pom.xml @@ -78,6 +78,17 @@ opentelemetry-context true + + io.opentelemetry + opentelemetry-sdk-testing + test + + + com.google.guava + guava-testlib + ${guava.version} + test + org.slf4j 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 new file mode 100644 index 0000000000..747170cade --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java @@ -0,0 +1,72 @@ +/* + * 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 io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.Meter; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * This class takes an OpenTelemetry object, and creates instruments (meters, histograms etc.) from + * it for recording golden signal metrics. There must be only one instance of + * GoldenSignalsMetricsRecorder per client, all the methods in this class are expected to be called + * from multiple threads, hence they need to be thread safe. + */ +class GoldenSignalsMetricsRecorder { + static final String CLIENT_REQUEST_DURATION_METRIC_NAME = "gcp.client.request.duration"; + static final String CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION = + "Measures the total time taken for a logical client request, including any retries, backoff, and pre/post-processing"; + + static final List BOUNDARIES = + Arrays.asList( + 0.0, 0.0001, 0.0005, 0.0010, 0.005, 0.010, 0.050, 0.100, 0.5, 1.0, 5.0, 10.0, 60.0, 300.0, + 900.0, 3600.0); + final DoubleHistogram clientRequestDurationRecorder; + + GoldenSignalsMetricsRecorder(OpenTelemetry openTelemetry, String libraryName) { + Meter meter = openTelemetry.meterBuilder(libraryName).build(); + + this.clientRequestDurationRecorder = + meter + .histogramBuilder(CLIENT_REQUEST_DURATION_METRIC_NAME) + .setDescription(CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION) + .setUnit("s") + .setExplicitBucketBoundariesAdvice(BOUNDARIES) + .build(); + } + + void recordOperationLatency(double operationLatency, Map attributes) { + clientRequestDurationRecorder.record( + operationLatency, ObservabilityUtils.toOtelAttributes(attributes)); + } +} 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/GoldenSignalsMetricsTracer.java new file mode 100644 index 0000000000..954feab58e --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java @@ -0,0 +1,98 @@ +/* + * 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.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.TimeUnit; + +/** + * This class computes golden signal metrics that can be observed in the lifecycle of an RPC + * operation. The responsibility of recording metrics should delegate to {@link + * GoldenSignalsMetricsRecorder}, hence this class should not have any knowledge about the + * observability framework (e.g. OpenTelemetry). + */ +class GoldenSignalsMetricsTracer implements ApiTracer { + private final Stopwatch clientRequestTimer; + private final GoldenSignalsMetricsRecorder metricsRecorder; + private final Map attributes = new HashMap<>(); + + /** + * Creates the following instruments for the following metrics: + * + *
    + *
  • Client Request Duration: Histogram + *
+ * + * @param metricsRecorder OpenTelemetry + */ + GoldenSignalsMetricsTracer(GoldenSignalsMetricsRecorder metricsRecorder) { + this.clientRequestTimer = Stopwatch.createStarted(); + this.metricsRecorder = metricsRecorder; + } + + @VisibleForTesting + GoldenSignalsMetricsTracer(GoldenSignalsMetricsRecorder metricsRecorder, Ticker ticker) { + this.clientRequestTimer = Stopwatch.createStarted(ticker); + this.metricsRecorder = metricsRecorder; + } + + /** + * The concept of "operation" and "client request" are the same. They both represent the total + * time taken for a logical client request, including any retries, backoff, and + * pre/post-processing + */ + @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); + } + + @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); + } + + @Override + public void operationFailed(Throwable error) { + attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); + 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/GoldenSignalsMetricsTracerFactory.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java new file mode 100644 index 0000000000..02aa9a1ff4 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import com.google.api.core.BetaApi; +import com.google.api.core.InternalApi; +import io.opentelemetry.api.OpenTelemetry; + +/** + * A {@link ApiTracerFactory} to build instances of {@link GoldenSignalsMetricsTracer}. + * + *

This class is expected to be initialized once during client initialization. + */ +@BetaApi +@InternalApi +public class GoldenSignalsMetricsTracerFactory implements ApiTracerFactory { + + private ApiTracerContext apiTracerContext; + private final OpenTelemetry openTelemetry; + private GoldenSignalsMetricsRecorder metricsRecorder; + + public GoldenSignalsMetricsTracerFactory(OpenTelemetry openTelemetry) { + this.openTelemetry = openTelemetry; + } + + @Override + public ApiTracer newTracer(ApiTracer parent, SpanName spanName, OperationType operationType) { + if (metricsRecorder == null) { + // This should never happen, in case it happens, create a no-op api tracer to not block + // regular requests. + return new BaseApiTracer(); + } + return new GoldenSignalsMetricsTracer(metricsRecorder); + } + + @Override + public ApiTracerFactory withContext(ApiTracerContext context) { + this.apiTracerContext = context; + this.metricsRecorder = + new GoldenSignalsMetricsRecorder( + openTelemetry, apiTracerContext.libraryMetadata().artifactName()); + return this; + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java index 16553dd118..e9ad908c21 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/MetricsTracer.java @@ -35,16 +35,13 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.core.ObsoleteApi; -import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.StatusCode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; -import javax.annotation.Nullable; /** * This class computes generic metrics that can be observed in the lifecycle of an RPC operation. @@ -123,7 +120,7 @@ public void operationFailed(Throwable error) { if (operationFinished.getAndSet(true)) { throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); } - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordOperationCount(1, attributes); @@ -175,7 +172,7 @@ public void attemptCancelled() { */ @Override public void attemptFailedDuration(Throwable error, java.time.Duration delay) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -199,7 +196,7 @@ public void attemptFailed(Throwable error, org.threeten.bp.Duration delay) { */ @Override public void attemptFailedRetriesExhausted(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } @@ -213,29 +210,11 @@ public void attemptFailedRetriesExhausted(Throwable error) { */ @Override public void attemptPermanentFailure(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); + attributes.put(STATUS_ATTRIBUTE, ObservabilityUtils.extractStatus(error)); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); } - /** Function to extract the status of the error as a string */ - @VisibleForTesting - static String extractStatus(@Nullable Throwable error) { - final String statusString; - - if (error == null) { - return StatusCode.Code.OK.toString(); - } else if (error instanceof CancellationException) { - statusString = StatusCode.Code.CANCELLED.toString(); - } else if (error instanceof ApiException) { - statusString = ((ApiException) error).getStatusCode().getCode().toString(); - } else { - statusString = StatusCode.Code.UNKNOWN.toString(); - } - - return statusString; - } - /** * Add attributes that will be attached to all metrics. This is expected to be called by * handwritten client teams to add additional attributes that are not supposed be collected by 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 ad55e20204..8f331d8094 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 @@ -58,4 +58,10 @@ public class ObservabilityAttributes { /** The RPC system name, e.g. 'grpc' or 'http'. */ public static final String RPC_SYSTEM_NAME_ATTRIBUTE = "rpc.system.name"; + + /** + * The error codes of the request. The value will be the string representation of the canonical + * gRPC status code (e.g., "OK", "INTERNAL"). + */ + public static final String RPC_RESPONSE_STATUS_ATTRIBUTE = "rpc.response.status_code"; } 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 new file mode 100644 index 0000000000..142a11ff11 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -0,0 +1,66 @@ +/* + * 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.StatusCode; +import com.google.common.base.Preconditions; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +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; + + if (error == null) { + return StatusCode.Code.OK.toString(); + } else if (error instanceof CancellationException) { + statusString = StatusCode.Code.CANCELLED.toString(); + } else if (error instanceof ApiException) { + statusString = ((ApiException) error).getStatusCode().getCode().toString(); + } else { + statusString = StatusCode.Code.UNKNOWN.toString(); + } + + return statusString; + } + + static Attributes toOtelAttributes(Map attributes) { + Preconditions.checkNotNull(attributes, "Attributes map cannot be null"); + AttributesBuilder attributesBuilder = Attributes.builder(); + attributes.forEach(attributesBuilder::put); + return attributesBuilder.build(); + } +} diff --git a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java index a029f42fde..3ccd6a6e7d 100644 --- a/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java @@ -33,11 +33,7 @@ import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; import com.google.api.gax.core.GaxProperties; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.Meter; @@ -116,7 +112,7 @@ public OpenTelemetryMetricsRecorder(OpenTelemetry openTelemetry, String serviceN */ @Override public void recordAttemptLatency(double attemptLatency, Map attributes) { - attemptLatencyRecorder.record(attemptLatency, toOtelAttributes(attributes)); + attemptLatencyRecorder.record(attemptLatency, ObservabilityUtils.toOtelAttributes(attributes)); } /** @@ -129,7 +125,7 @@ public void recordAttemptLatency(double attemptLatency, Map attr */ @Override public void recordAttemptCount(long count, Map attributes) { - attemptCountRecorder.add(count, toOtelAttributes(attributes)); + attemptCountRecorder.add(count, ObservabilityUtils.toOtelAttributes(attributes)); } /** @@ -141,7 +137,8 @@ public void recordAttemptCount(long count, Map attributes) { */ @Override public void recordOperationLatency(double operationLatency, Map attributes) { - operationLatencyRecorder.record(operationLatency, toOtelAttributes(attributes)); + operationLatencyRecorder.record( + operationLatency, ObservabilityUtils.toOtelAttributes(attributes)); } /** @@ -154,14 +151,6 @@ public void recordOperationLatency(double operationLatency, Map */ @Override public void recordOperationCount(long count, Map attributes) { - operationCountRecorder.add(count, toOtelAttributes(attributes)); - } - - @VisibleForTesting - Attributes toOtelAttributes(Map attributes) { - Preconditions.checkNotNull(attributes, "Attributes map cannot be null"); - AttributesBuilder attributesBuilder = Attributes.builder(); - attributes.forEach(attributesBuilder::put); - return attributesBuilder.build(); + operationCountRecorder.add(count, ObservabilityUtils.toOtelAttributes(attributes)); } } 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 new file mode 100644 index 0000000000..3476e64a72 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java @@ -0,0 +1,117 @@ +/* + * 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.api.gax.tracing.GoldenSignalsMetricsRecorder.BOUNDARIES; +import static com.google.api.gax.tracing.GoldenSignalsMetricsRecorder.CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION; +import static com.google.api.gax.tracing.GoldenSignalsMetricsRecorder.CLIENT_REQUEST_DURATION_METRIC_NAME; +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +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.util.Collection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GoldenSignalsMetricsRecorderTest { + private static final String ARTIFACT_NAME = "test-library"; + private static final String ATTRIBUTE_1 = "attribute_1"; + private static final String VALUE_1 = "value_1"; + + private InMemoryMetricReader metricReader; + + private GoldenSignalsMetricsRecorder recorder; + + @BeforeEach + void setUp() { + metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); + recorder = new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME); + } + + @Test + void recordOperationLatency_shouldRecordMeterInfo() { + recorder.recordOperationLatency(0.012, ImmutableMap.of(ATTRIBUTE_1, VALUE_1)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getName()).isEqualTo(CLIENT_REQUEST_DURATION_METRIC_NAME); + assertThat(metricData.getDescription()).isEqualTo(CLIENT_REQUEST_DURATION_METRIC_DESCRIPTION); + assertThat(metricData.getUnit()).isEqualTo("s"); + assertThat(metricData.getInstrumentationScopeInfo().getName()).isEqualTo(ARTIFACT_NAME); + } + + @Test + void recordOperationLatency_shouldRecordWithBoundaries() { + recorder.recordOperationLatency(0.012, ImmutableMap.of(ATTRIBUTE_1, VALUE_1)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints().iterator().next().getBoundaries()) + .isEqualTo(BOUNDARIES); + } + + @Test + void recordOperationLatency_shouldRecordMetrics() { + recorder.recordOperationLatency(0.012, ImmutableMap.of(ATTRIBUTE_1, VALUE_1)); + + 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().getMax()).isNonZero(); + } + + @Test + void recordOperationLatency_shouldRecordMetricAttributes() { + recorder.recordOperationLatency(0.012, ImmutableMap.of(ATTRIBUTE_1, VALUE_1)); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo(Attributes.of(AttributeKey.stringKey(ATTRIBUTE_1), VALUE_1)); + } +} 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 new file mode 100644 index 0000000000..8a39ff27f1 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactoryTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; + +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 newTracer_createsTracer_successfully() { + tracerFactory.withContext(ApiTracerContext.empty()); + ApiTracer actual = + tracerFactory.newTracer( + mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); + assertThat(actual).isInstanceOf(GoldenSignalsMetricsTracer.class); + } + + @Test + void newTracer_createsBaseTracer_ifMetricsRecorderIsNull() { + ApiTracer actual = + tracerFactory.newTracer( + mock(ApiTracer.class), mock(SpanName.class), ApiTracerFactory.OperationType.Unary); + assertThat(actual).isInstanceOf(BaseApiTracer.class); + } +} 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/GoldenSignalsMetricsTracerTest.java new file mode 100644 index 0000000000..cfd9588abb --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java @@ -0,0 +1,167 @@ +/* + * 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.api.gax.tracing.ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE; +import static com.google.common.truth.Truth.assertThat; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.common.testing.FakeTicker; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +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.util.Collection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GoldenSignalsMetricsTracerTest { + 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 FakeTicker ticker; + + @BeforeEach + void setUp() { + metricReader = InMemoryMetricReader.create(); + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); + ticker = new FakeTicker(); + tracer = + new GoldenSignalsMetricsTracer( + new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME), ticker); + } + + @Test + void operationSucceeded_shouldRecordsDuration() { + ticker.advance(TEST_REQUEST_DURATION_NANO); + tracer.operationSucceeded(); + + 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().getMax()) + .isEqualTo(EXPECTED_REQUEST_DURATION_SECOND); + } + + @Test + void operationSucceeded_shouldRecordsOKStatus() { + tracer.operationSucceeded(); + + 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(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.OK.toString())); + } + + @Test + void operationCancelled_shouldRecordsDuration() { + ticker.advance(TEST_REQUEST_DURATION_NANO); + tracer.operationCancelled(); + + 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().getMax()) + .isEqualTo(EXPECTED_REQUEST_DURATION_SECOND); + } + + @Test + void operationCancelled_shouldRecordsOKStatus() { + tracer.operationCancelled(); + + 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(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.CANCELLED.toString())); + } + + @Test + void operationFailed_shouldRecordsDuration() { + ticker.advance(TEST_REQUEST_DURATION_NANO); + ApiException error = + new ApiException("test error", null, new FakeStatusCode(StatusCode.Code.INTERNAL), false); + 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().getMax()) + .isEqualTo(EXPECTED_REQUEST_DURATION_SECOND); + } + + @Test + void operationFailed_shouldRecordsOKStatus() { + ApiException error = + new ApiException("test error", null, new FakeStatusCode(StatusCode.Code.INTERNAL), false); + 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(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.INTERNAL.toString())); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerTest.java index 1864eba7e1..b16820c624 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/MetricsTracerTest.java @@ -238,27 +238,4 @@ void testAddAttributes_recordsAttributesWithMap() { assertThat(metricsTracer.getAttributes().get("FakeTableId")).isEqualTo("12345"); assertThat(metricsTracer.getAttributes().get("FakeInstanceId")).isEqualTo("67890"); } - - @Test - void testExtractStatus_errorConversion_apiExceptions() { - ApiException error = - new ApiException("fake_error", null, new FakeStatusCode(Code.INVALID_ARGUMENT), false); - String errorCode = metricsTracer.extractStatus(error); - assertThat(errorCode).isEqualTo(Code.INVALID_ARGUMENT.toString()); - } - - @Test - void testExtractStatus_errorConversion_noError() { - // test "OK", which corresponds to a "null" error. - String successCode = metricsTracer.extractStatus(null); - assertThat(successCode).isEqualTo(Code.OK.toString()); - } - - @Test - void testExtractStatus_errorConversion_unknownException() { - // test "UNKNOWN" - Throwable unknownException = new RuntimeException(); - String errorCode2 = metricsTracer.extractStatus(unknownException); - assertThat(errorCode2).isEqualTo(Code.UNKNOWN.toString()); - } } 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 new file mode 100644 index 0000000000..9d9a07686c --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/ObservabilityUtilsTest.java @@ -0,0 +1,93 @@ +/* + * 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.junit.jupiter.api.Assertions.assertThrows; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.common.collect.ImmutableMap; +import com.google.common.truth.Truth; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ObservabilityUtilsTest { + @Test + 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()); + } + + @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()); + } + + @Test + void testExtractStatus_errorConversion_unknownException() { + // test "UNKNOWN" + Throwable unknownException = new RuntimeException(); + String errorCode2 = ObservabilityUtils.extractStatus(unknownException); + assertThat(errorCode2).isEqualTo(StatusCode.Code.UNKNOWN.toString()); + } + + @Test + void testToOtelAttributes_correctConversion() { + String attribute1 = "attribute_1"; + String attribute2 = "attribute_2"; + String attribute1Value = "Today is a good day"; + String attribute2Value = "Does not matter"; + Map attributes = + ImmutableMap.of(attribute1, attribute1Value, attribute2, attribute2Value); + + Attributes otelAttributes = ObservabilityUtils.toOtelAttributes(attributes); + + Truth.assertThat(otelAttributes.get(AttributeKey.stringKey(attribute1))) + .isEqualTo(attribute1Value); + Truth.assertThat(otelAttributes.get(AttributeKey.stringKey(attribute2))) + .isEqualTo(attribute2Value); + } + + @Test + void testToOtelAttributes_nullInput() { + Throwable thrown = + assertThrows(NullPointerException.class, () -> ObservabilityUtils.toOtelAttributes(null)); + Truth.assertThat(thrown).hasMessageThat().contains("Attributes map cannot be null"); + } +} diff --git a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorderTest.java b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorderTest.java index 3eea5955a4..7a49105a6c 100644 --- a/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorderTest.java +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorderTest.java @@ -29,15 +29,12 @@ */ package com.google.api.gax.tracing; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import com.google.common.collect.ImmutableMap; -import com.google.common.truth.Truth; import com.google.rpc.Code; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.DoubleHistogramBuilder; @@ -104,7 +101,7 @@ private Map getAttributes(Code statusCode) { void testAttemptCountRecorder_recordsAttributes() { Map attributes = getAttributes(Code.OK); - Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + Attributes otelAttributes = ObservabilityUtils.toOtelAttributes(attributes); otelMetricsRecorder.recordAttemptCount(1, attributes); verify(attemptCountRecorder).add(1, otelAttributes); @@ -115,7 +112,7 @@ void testAttemptCountRecorder_recordsAttributes() { void testAttemptLatencyRecorder_recordsAttributes() { Map attributes = getAttributes(Code.NOT_FOUND); - Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + Attributes otelAttributes = ObservabilityUtils.toOtelAttributes(attributes); otelMetricsRecorder.recordAttemptLatency(1.1, attributes); verify(attemptLatencyRecorder).record(1.1, otelAttributes); @@ -126,7 +123,7 @@ void testAttemptLatencyRecorder_recordsAttributes() { void testOperationCountRecorder_recordsAttributes() { Map attributes = getAttributes(Code.OK); - Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + Attributes otelAttributes = ObservabilityUtils.toOtelAttributes(attributes); otelMetricsRecorder.recordOperationCount(1, attributes); verify(operationCountRecorder).add(1, otelAttributes); @@ -137,34 +134,13 @@ void testOperationCountRecorder_recordsAttributes() { void testOperationLatencyRecorder_recordsAttributes() { Map attributes = getAttributes(Code.INVALID_ARGUMENT); - Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + Attributes otelAttributes = ObservabilityUtils.toOtelAttributes(attributes); otelMetricsRecorder.recordOperationLatency(1.7, attributes); verify(operationLatencyRecorder).record(1.7, otelAttributes); verifyNoMoreInteractions(operationLatencyRecorder); } - @Test - void testToOtelAttributes_correctConversion() { - Map attributes = getAttributes(Code.OK); - - Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); - - Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("status"))) - .isEqualTo(Code.OK.toString()); - Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("method_name"))) - .isEqualTo(DEFAULT_METHOD_NAME); - Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("language"))) - .isEqualTo(MetricsTracer.DEFAULT_LANGUAGE); - } - - @Test - void testToOtelAttributes_nullInput() { - Throwable thrown = - assertThrows(NullPointerException.class, () -> otelMetricsRecorder.toOtelAttributes(null)); - Truth.assertThat(thrown).hasMessageThat().contains("Attributes map cannot be null"); - } - private void setupAttemptCountRecorder() { // Configure chained mocking for AttemptCountRecorder Mockito.when(meter.counterBuilder(ATTEMPT_COUNT)).thenReturn(attemptCountRecorderBuilder);