diff --git a/gapic-generator-java-bom/pom.xml b/gapic-generator-java-bom/pom.xml index 026e0b016b..6d44de60f6 100644 --- a/gapic-generator-java-bom/pom.xml +++ b/gapic-generator-java-bom/pom.xml @@ -70,6 +70,13 @@ pom import + + io.opentelemetry + opentelemetry-bom + ${opentelemetry.version} + pom + import + diff --git a/gapic-generator-java-pom-parent/pom.xml b/gapic-generator-java-pom-parent/pom.xml index d4529348c7..883f97629a 100644 --- a/gapic-generator-java-pom-parent/pom.xml +++ b/gapic-generator-java-pom-parent/pom.xml @@ -32,6 +32,7 @@ 2.10.1 32.1.3-jre 3.25.2 + 1.35.0 8 diff --git a/gax-java/dependencies.properties b/gax-java/dependencies.properties index ef85eca3c4..e5566ec004 100644 --- a/gax-java/dependencies.properties +++ b/gax-java/dependencies.properties @@ -39,6 +39,7 @@ 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.36.0 maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.23.0 maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.23.0 +maven.io_opentelemetry_opentelemetry_api=io.opentelemetry:opentelemetry-api:1.35.0 maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.31.1 maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.31.1 maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.31.1 diff --git a/gax-java/gax/BUILD.bazel b/gax-java/gax/BUILD.bazel index 85c41f8b58..309564c87f 100644 --- a/gax-java/gax/BUILD.bazel +++ b/gax-java/gax/BUILD.bazel @@ -18,6 +18,7 @@ _COMPILE_DEPS = [ "@com_google_code_findbugs_jsr305//jar", "@com_google_errorprone_error_prone_annotations//jar", "@com_google_guava_guava//jar", + "@io_opentelemetry_opentelemetry_api//jar", "@io_opencensus_opencensus_api//jar", "@io_opencensus_opencensus_contrib_http_util//jar", "@io_grpc_grpc_java//context:context", diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml index e56ebadb17..5b82f001bc 100644 --- a/gax-java/gax/pom.xml +++ b/gax-java/gax/pom.xml @@ -57,6 +57,10 @@ graal-sdk provided + + io.opentelemetry + opentelemetry-api + 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 45a8558599..7938bde82b 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 @@ -40,6 +40,7 @@ import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import javax.annotation.Nullable; import org.threeten.bp.Duration; @@ -47,33 +48,42 @@ * This class computes generic metrics that can be observed in the lifecycle of an RPC operation. * The responsibility of recording metrics should delegate to {@link MetricsRecorder}, hence this * class should not have any knowledge about the observability framework used for metrics recording. + * method_name and language will be autopopulated attributes. Default value of language is 'Java'. */ @BetaApi @InternalApi public class MetricsTracer implements ApiTracer { - - private static final String STATUS_ATTRIBUTE = "status"; - + public static final String METHOD_NAME_ATTRIBUTE = "method_name"; + public static final String LANGUAGE_ATTRIBUTE = "language"; + public static final String STATUS_ATTRIBUTE = "status"; + public static final String DEFAULT_LANGUAGE = "Java"; + private static final String OPERATION_FINISHED_STATUS_MESSAGE = + "Operation has already been completed"; private Stopwatch attemptTimer; - private final Stopwatch operationTimer = Stopwatch.createStarted(); - private final Map attributes = new HashMap<>(); - - private MetricsRecorder metricsRecorder; + private final MetricsRecorder metricsRecorder; + private final AtomicBoolean operationFinished; public MetricsTracer(MethodName methodName, MetricsRecorder metricsRecorder) { - this.attributes.put("method_name", methodName.toString()); + this.attributes.put(METHOD_NAME_ATTRIBUTE, methodName.toString()); + this.attributes.put(LANGUAGE_ATTRIBUTE, DEFAULT_LANGUAGE); this.metricsRecorder = metricsRecorder; + this.operationFinished = new AtomicBoolean(); } /** * Signals that the overall operation has finished successfully. The tracer is now considered * closed and should no longer be used. Successful operation adds "OK" value to the status * attribute key. + * + * @throws IllegalStateException if an operation completion call has already been invoked */ @Override public void operationSucceeded() { + if (operationFinished.getAndSet(true)) { + throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); + } attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.OK.toString()); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); @@ -84,9 +94,14 @@ public void operationSucceeded() { * Signals that the operation was cancelled by the user. The tracer is now considered closed and * should no longer be used. Cancelled operation adds "CANCELLED" value to the status attribute * key. + * + * @throws IllegalStateException if an operation completion call has already been invoked */ @Override public void operationCancelled() { + if (operationFinished.getAndSet(true)) { + throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); + } attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString()); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); @@ -97,9 +112,14 @@ public void operationCancelled() { * Signals that the operation was cancelled by the user. The tracer is now considered closed and * should no longer be used. Failed operation extracts the error from the throwable and adds it to * the status attribute key. + * + * @throws IllegalStateException if an operation completion call has already been invoked */ @Override public void operationFailed(Throwable error) { + if (operationFinished.getAndSet(true)) { + throw new IllegalStateException(OPERATION_FINISHED_STATUS_MESSAGE); + } attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); metricsRecorder.recordOperationLatency( operationTimer.elapsed(TimeUnit.MILLISECONDS), attributes); @@ -126,7 +146,6 @@ public void attemptStarted(Object request, int attemptNumber) { */ @Override public void attemptSucceeded() { - attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.OK.toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); @@ -138,7 +157,6 @@ public void attemptSucceeded() { */ @Override public void attemptCancelled() { - attributes.put(STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString()); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); @@ -154,7 +172,6 @@ public void attemptCancelled() { */ @Override public void attemptFailed(Throwable error, Duration delay) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); @@ -169,7 +186,6 @@ public void attemptFailed(Throwable error, Duration delay) { */ @Override public void attemptFailedRetriesExhausted(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); metricsRecorder.recordAttemptLatency(attemptTimer.elapsed(TimeUnit.MILLISECONDS), attributes); metricsRecorder.recordAttemptCount(1, attributes); @@ -184,7 +200,6 @@ public void attemptFailedRetriesExhausted(Throwable error) { */ @Override public void attemptPermanentFailure(Throwable error) { - attributes.put(STATUS_ATTRIBUTE, extractStatus(error)); 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/OpenTelemetryMetricsRecorder.java b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java new file mode 100644 index 0000000000..fdf1dd2d09 --- /dev/null +++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorder.java @@ -0,0 +1,165 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.api.gax.tracing; + +import com.google.api.core.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; +import java.util.Map; + +/** + * OpenTelemetry implementation of recording metrics. This implementation collections the + * measurements related to the lifecyle of an RPC. + * + *

For the Otel implementation, an attempt is a single RPC invocation and an operation is the + * collection of all the attempts made before a response is returned (either as a success or an + * error). A single call (i.e. `EchoClient.echo()`) should have an operation_count of 1 and may have + * an attempt_count of 1+ (depending on the retry configurations). + */ +@BetaApi +@InternalApi +public class OpenTelemetryMetricsRecorder implements MetricsRecorder { + private final DoubleHistogram attemptLatencyRecorder; + private final DoubleHistogram operationLatencyRecorder; + private final LongCounter operationCountRecorder; + private final LongCounter attemptCountRecorder; + + /** + * Creates the following instruments for the following metrics: + * + *

+ * + * @param openTelemetry OpenTelemetry instance + * @param serviceName Service Name + */ + public OpenTelemetryMetricsRecorder(OpenTelemetry openTelemetry, String serviceName) { + Meter meter = + openTelemetry + .meterBuilder("gax-java") + .setInstrumentationVersion(GaxProperties.getGaxVersion()) + .build(); + this.attemptLatencyRecorder = + meter + .histogramBuilder(serviceName + "/attempt_latency") + .setDescription("Time an individual attempt took") + .setUnit("ms") + .build(); + this.operationLatencyRecorder = + meter + .histogramBuilder(serviceName + "/operation_latency") + .setDescription( + "Total time until final operation success or failure, including retries and backoff.") + .setUnit("ms") + .build(); + this.attemptCountRecorder = + meter + .counterBuilder(serviceName + "/attempt_count") + .setDescription("Number of Attempts") + .setUnit("1") + .build(); + this.operationCountRecorder = + meter + .counterBuilder(serviceName + "/operation_count") + .setDescription("Number of Operations") + .setUnit("1") + .build(); + } + + /** + * Record the latency for an individual attempt. Data is stored in a Histogram. + * + * @param attemptLatency Attempt Latency in ms + * @param attributes Map of the attributes to store + */ + @Override + public void recordAttemptLatency(double attemptLatency, Map attributes) { + attemptLatencyRecorder.record(attemptLatency, toOtelAttributes(attributes)); + } + + /** + * Record an attempt made. The attempt count number is stored in a LongCounter. + * + *

The count should be set as 1 every time this is invoked (each retry attempt) + * + * @param count The number of attempts made + * @param attributes Map of the attributes to store + */ + @Override + public void recordAttemptCount(long count, Map attributes) { + attemptCountRecorder.add(count, toOtelAttributes(attributes)); + } + + /** + * Record the latency for the entire operation. This is the latency for the entire RPC, including + * all the retry attempts + * + * @param operationLatency Operation Latency in ms + * @param attributes Map of the attributes to store + */ + @Override + public void recordOperationLatency(double operationLatency, Map attributes) { + operationLatencyRecorder.record(operationLatency, toOtelAttributes(attributes)); + } + + /** + * Record an operation made. The operation count number is stored in a LongCounter. + * + *

The operation count should always be 1 and this should be invoked once. + * + * @param count The number of operations made + * @param attributes Map of the attributes to store + */ + @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(); + } +} 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 7b6b76f181..817c53656c 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 @@ -30,6 +30,7 @@ package com.google.api.gax.tracing; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; @@ -41,7 +42,6 @@ import com.google.api.gax.rpc.StatusCode.Code; import com.google.api.gax.rpc.testing.FakeStatusCode; import com.google.common.collect.ImmutableMap; -import com.google.common.truth.Truth; import java.util.Map; import org.junit.Before; import org.junit.Rule; @@ -56,6 +56,7 @@ @RunWith(JUnit4.class) public class MetricsTracerTest { + private static final String DEFAULT_METHOD_NAME = "fake_service.fake_method"; // stricter way of testing for early detection of unused stubs and argument mismatches @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); @@ -69,15 +70,21 @@ public void setUp() { new MetricsTracer(MethodName.of("fake_service", "fake_method"), metricsRecorder); } + private ImmutableMap getAttributes(Code statusCode) { + return ImmutableMap.of( + "status", + statusCode.toString(), + "method_name", + DEFAULT_METHOD_NAME, + "language", + MetricsTracer.DEFAULT_LANGUAGE); + } + @Test public void testOperationSucceeded_recordsAttributes() { - metricsTracer.operationSucceeded(); - Map attributes = - ImmutableMap.of( - "status", "OK", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.OK); verify(metricsRecorder).recordOperationCount(1, attributes); verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes)); @@ -87,16 +94,12 @@ public void testOperationSucceeded_recordsAttributes() { @Test public void testOperationFailed_recordsAttributes() { - ApiException error0 = new NotFoundException( "invalid argument", null, new FakeStatusCode(Code.INVALID_ARGUMENT), false); metricsTracer.operationFailed(error0); - Map attributes = - ImmutableMap.of( - "status", "INVALID_ARGUMENT", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.INVALID_ARGUMENT); verify(metricsRecorder).recordOperationCount(1, attributes); verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes)); @@ -106,13 +109,9 @@ public void testOperationFailed_recordsAttributes() { @Test public void testOperationCancelled_recordsAttributes() { - metricsTracer.operationCancelled(); - Map attributes = - ImmutableMap.of( - "status", "CANCELLED", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.CANCELLED); verify(metricsRecorder).recordOperationCount(1, attributes); verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes)); @@ -129,10 +128,7 @@ public void testAttemptSucceeded_recordsAttributes() { metricsTracer.attemptStarted(mockSuccessfulRequest, 0); metricsTracer.attemptSucceeded(); - Map attributes = - ImmutableMap.of( - "status", "OK", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.OK); verify(metricsRecorder).recordAttemptCount(1, attributes); verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes)); @@ -152,10 +148,7 @@ public void testAttemptFailed_recordsAttributes() { "invalid argument", null, new FakeStatusCode(Code.INVALID_ARGUMENT), false); metricsTracer.attemptFailed(error0, Duration.ofMillis(2)); - Map attributes = - ImmutableMap.of( - "status", "INVALID_ARGUMENT", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.INVALID_ARGUMENT); verify(metricsRecorder).recordAttemptCount(1, attributes); verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes)); @@ -171,10 +164,7 @@ public void testAttemptCancelled_recordsAttributes() { metricsTracer.attemptStarted(mockCancelledRequest, 0); metricsTracer.attemptCancelled(); - Map attributes = - ImmutableMap.of( - "status", "CANCELLED", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.CANCELLED); verify(metricsRecorder).recordAttemptCount(1, attributes); verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes)); @@ -193,10 +183,7 @@ public void testAttemptFailedRetriesExhausted_recordsAttributes() { "deadline exceeded", null, new FakeStatusCode(Code.DEADLINE_EXCEEDED), false); metricsTracer.attemptFailedRetriesExhausted(error0); - Map attributes = - ImmutableMap.of( - "status", "DEADLINE_EXCEEDED", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.DEADLINE_EXCEEDED); verify(metricsRecorder).recordAttemptCount(1, attributes); verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes)); @@ -206,7 +193,6 @@ public void testAttemptFailedRetriesExhausted_recordsAttributes() { @Test public void testAttemptPermanentFailure_recordsAttributes() { - // initialize mock-request Object mockRequest = new Object(); // Attempt #1 @@ -215,10 +201,7 @@ public void testAttemptPermanentFailure_recordsAttributes() { new NotFoundException("not found", null, new FakeStatusCode(Code.NOT_FOUND), false); metricsTracer.attemptFailedRetriesExhausted(error0); - Map attributes = - ImmutableMap.of( - "status", "NOT_FOUND", - "method_name", "fake_service.fake_method"); + Map attributes = getAttributes(Code.NOT_FOUND); verify(metricsRecorder).recordAttemptCount(1, attributes); verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes)); @@ -227,35 +210,42 @@ public void testAttemptPermanentFailure_recordsAttributes() { } @Test - public void testAddAttributes_recordsAttributes() { + public void testMultipleOperationCalls_throwsError() { + metricsTracer.operationSucceeded(); + IllegalStateException exception1 = + assertThrows(IllegalStateException.class, () -> metricsTracer.operationCancelled()); + assertThat(exception1.getMessage()).isEqualTo("Operation has already been completed"); + IllegalStateException exception2 = + assertThrows(IllegalStateException.class, () -> metricsTracer.operationSucceeded()); + assertThat(exception2.getMessage()).isEqualTo("Operation has already been completed"); + } + @Test + public void testAddAttributes_recordsAttributes() { metricsTracer.addAttributes("FakeTableId", "12345"); - Truth.assertThat(metricsTracer.getAttributes().get("FakeTableId").equals("12345")); + assertThat(metricsTracer.getAttributes().get("FakeTableId")).isEqualTo("12345"); } @Test public 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("INVALID_ARGUMENT"); + assertThat(errorCode).isEqualTo(Code.INVALID_ARGUMENT.toString()); } @Test public void testExtractStatus_errorConversion_noError() { - // test "OK", which corresponds to a "null" error. String successCode = metricsTracer.extractStatus(null); - assertThat(successCode).isEqualTo("OK"); + assertThat(successCode).isEqualTo(Code.OK.toString()); } @Test public void testExtractStatus_errorConversion_unknownException() { - // test "UNKNOWN" Throwable unknownException = new RuntimeException(); String errorCode2 = metricsTracer.extractStatus(unknownException); - assertThat(errorCode2).isEqualTo("UNKNOWN"); + assertThat(errorCode2).isEqualTo(Code.UNKNOWN.toString()); } } 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 new file mode 100644 index 0000000000..b0ce0cb927 --- /dev/null +++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpenTelemetryMetricsRecorderTest.java @@ -0,0 +1,215 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.tracing; + +import static org.junit.Assert.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; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; +import java.util.Map; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.quality.Strictness; + +@RunWith(JUnit4.class) +public class OpenTelemetryMetricsRecorderTest { + private static final String SERVICE_NAME = "OtelRecorderTest"; + private static final String ATTEMPT_COUNT = SERVICE_NAME + "/attempt_count"; + private static final String OPERATION_COUNT = SERVICE_NAME + "/operation_count"; + private static final String ATTEMPT_LATENCY = SERVICE_NAME + "/attempt_latency"; + private static final String OPERATION_LATENCY = SERVICE_NAME + "/operation_latency"; + private static final String DEFAULT_METHOD_NAME = "fake_service.fake_method"; + // stricter way of testing for early detection of unused stubs and argument mismatches + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS); + + private OpenTelemetryMetricsRecorder otelMetricsRecorder; + @Mock private OpenTelemetry openTelemetry; + @Mock private Meter meter; + @Mock private MeterBuilder meterBuilder; + @Mock private LongCounter attemptCountRecorder; + @Mock private LongCounterBuilder attemptCountRecorderBuilder; + @Mock private DoubleHistogramBuilder attemptLatencyRecorderBuilder; + @Mock private DoubleHistogram attemptLatencyRecorder; + @Mock private DoubleHistogram operationLatencyRecorder; + @Mock private DoubleHistogramBuilder operationLatencyRecorderBuilder; + @Mock private LongCounter operationCountRecorder; + @Mock private LongCounterBuilder operationCountRecorderBuilder; + + @Before + public void setUp() { + Mockito.when(openTelemetry.meterBuilder(Mockito.anyString())).thenReturn(meterBuilder); + Mockito.when(meterBuilder.setInstrumentationVersion(Mockito.anyString())) + .thenReturn(meterBuilder); + Mockito.when(meterBuilder.build()).thenReturn(meter); + // setup mocks for all the recorders using chained mocking + setupAttemptCountRecorder(); + setupAttemptLatencyRecorder(); + setupOperationLatencyRecorder(); + setupOperationCountRecorder(); + + otelMetricsRecorder = new OpenTelemetryMetricsRecorder(openTelemetry, SERVICE_NAME); + } + + private Map getAttributes(Code statusCode) { + return ImmutableMap.of( + "status", + statusCode.toString(), + "method_name", + DEFAULT_METHOD_NAME, + "language", + MetricsTracer.DEFAULT_LANGUAGE); + } + + @Test + public void testAttemptCountRecorder_recordsAttributes() { + Map attributes = getAttributes(Code.OK); + + Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + otelMetricsRecorder.recordAttemptCount(1, attributes); + + verify(attemptCountRecorder).add(1, otelAttributes); + verifyNoMoreInteractions(attemptCountRecorder); + } + + @Test + public void testAttemptLatencyRecorder_recordsAttributes() { + Map attributes = getAttributes(Code.NOT_FOUND); + + Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + otelMetricsRecorder.recordAttemptLatency(1.1, attributes); + + verify(attemptLatencyRecorder).record(1.1, otelAttributes); + verifyNoMoreInteractions(attemptLatencyRecorder); + } + + @Test + public void testOperationCountRecorder_recordsAttributes() { + Map attributes = getAttributes(Code.OK); + + Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + otelMetricsRecorder.recordOperationCount(1, attributes); + + verify(operationCountRecorder).add(1, otelAttributes); + verifyNoMoreInteractions(operationCountRecorder); + } + + @Test + public void testOperationLatencyRecorder_recordsAttributes() { + Map attributes = getAttributes(Code.INVALID_ARGUMENT); + + Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes); + otelMetricsRecorder.recordOperationLatency(1.7, attributes); + + verify(operationLatencyRecorder).record(1.7, otelAttributes); + verifyNoMoreInteractions(operationLatencyRecorder); + } + + @Test + public 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 + public 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); + Mockito.when(attemptCountRecorderBuilder.setDescription(Mockito.anyString())) + .thenReturn(attemptCountRecorderBuilder); + Mockito.when(attemptCountRecorderBuilder.setUnit(Mockito.anyString())) + .thenReturn(attemptCountRecorderBuilder); + Mockito.when(attemptCountRecorderBuilder.build()).thenReturn(attemptCountRecorder); + } + + private void setupOperationCountRecorder() { + // Configure chained mocking for operationCountRecorder + Mockito.when(meter.counterBuilder(OPERATION_COUNT)).thenReturn(operationCountRecorderBuilder); + Mockito.when(operationCountRecorderBuilder.setDescription(Mockito.anyString())) + .thenReturn(operationCountRecorderBuilder); + Mockito.when(operationCountRecorderBuilder.setUnit("1")) + .thenReturn(operationCountRecorderBuilder); + Mockito.when(operationCountRecorderBuilder.build()).thenReturn(operationCountRecorder); + } + + private void setupAttemptLatencyRecorder() { + // Configure chained mocking for attemptLatencyRecorder + Mockito.when(meter.histogramBuilder(ATTEMPT_LATENCY)).thenReturn(attemptLatencyRecorderBuilder); + Mockito.when(attemptLatencyRecorderBuilder.setDescription(Mockito.anyString())) + .thenReturn(attemptLatencyRecorderBuilder); + Mockito.when(attemptLatencyRecorderBuilder.setUnit(Mockito.anyString())) + .thenReturn(attemptLatencyRecorderBuilder); + Mockito.when(attemptLatencyRecorderBuilder.build()).thenReturn(attemptLatencyRecorder); + } + + private void setupOperationLatencyRecorder() { + // Configure chained mocking for operationLatencyRecorder + Mockito.when(meter.histogramBuilder(OPERATION_LATENCY)) + .thenReturn(operationLatencyRecorderBuilder); + Mockito.when(operationLatencyRecorderBuilder.setDescription(Mockito.anyString())) + .thenReturn(operationLatencyRecorderBuilder); + Mockito.when(operationLatencyRecorderBuilder.setUnit("ms")) + .thenReturn(operationLatencyRecorderBuilder); + Mockito.when(operationLatencyRecorderBuilder.build()).thenReturn(operationLatencyRecorder); + } +} diff --git a/gax-java/pom.xml b/gax-java/pom.xml index 0eea8df344..83cdcde9a3 100644 --- a/gax-java/pom.xml +++ b/gax-java/pom.xml @@ -158,6 +158,13 @@ pom import + + io.opentelemetry + opentelemetry-bom + ${opentelemetry.version} + pom + import + diff --git a/java-shared-dependencies/third-party-dependencies/pom.xml b/java-shared-dependencies/third-party-dependencies/pom.xml index 7c914da657..708caff83a 100644 --- a/java-shared-dependencies/third-party-dependencies/pom.xml +++ b/java-shared-dependencies/third-party-dependencies/pom.xml @@ -182,16 +182,6 @@ j2objc-annotations ${j2objc-annotations.version} - - io.opentelemetry - opentelemetry-api - ${opentelemetry.version} - - - io.opentelemetry - opentelemetry-context - ${opentelemetry.version} - \ No newline at end of file diff --git a/showcase/gapic-showcase/pom.xml b/showcase/gapic-showcase/pom.xml index 81313b780d..44fc03cb7d 100644 --- a/showcase/gapic-showcase/pom.xml +++ b/showcase/gapic-showcase/pom.xml @@ -182,5 +182,22 @@ grpc-google-iam-v1 test + + + + io.opentelemetry + opentelemetry-api + test + + + io.opentelemetry + opentelemetry-sdk + test + + + io.opentelemetry + opentelemetry-sdk-testing + test + diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java new file mode 100644 index 0000000000..317897710b --- /dev/null +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java @@ -0,0 +1,781 @@ +/* + * Copyright 2024 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.showcase.v1beta1.it; + +import static org.junit.Assert.assertThrows; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.core.ApiFuture; +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.Code; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.api.gax.rpc.UnavailableException; +import com.google.api.gax.tracing.MetricsTracer; +import com.google.api.gax.tracing.MetricsTracerFactory; +import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.truth.Truth; +import com.google.protobuf.Duration; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.BlockRequest; +import com.google.showcase.v1beta1.BlockResponse; +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.EchoStub; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import io.grpc.ManagedChannelBuilder; +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.Data; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.LongPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.data.PointData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +/** + * Showcase Test to confirm that metrics are being collected and that the correct metrics are being + * recorded. Utilizes an in-memory metric reader to collect the data. + * + *

Every test flows through the same way and runs through the same assertions. First, all th + * metrics are pulled in via {@link #getMetricDataList()} which are polled until all the metrics are + * collected. Then the test will attempt check that reader collected the correct number of data + * points in {@link #verifyPointDataSum(List, int)}. Then, check that the attributes to be collected + * via {@link #verifyStatusAttribute(List, List)}. Finally, check that the status for each attempt + * is correct. + */ +public class ITOtelMetrics { + private static final int DEFAULT_OPERATION_COUNT = 1; + private static final String SERVICE_NAME = "ShowcaseTest"; + private static final String ATTEMPT_COUNT = SERVICE_NAME + "/attempt_count"; + private static final String OPERATION_COUNT = SERVICE_NAME + "/operation_count"; + private static final String ATTEMPT_LATENCY = SERVICE_NAME + "/attempt_latency"; + private static final String OPERATION_LATENCY = SERVICE_NAME + "/operation_latency"; + private static final int NUM_METRICS = 4; + private static final int NUM_COLLECTION_FLUSH_ATTEMPTS = 10; + private InMemoryMetricReader inMemoryMetricReader; + private EchoClient grpcClient; + private EchoClient httpClient; + + /** + * Internal class in the Otel Showcases test used to assert that number of status codes recorded. + */ + private static class StatusCount { + private final Code statusCode; + private final int count; + + public StatusCount(Code statusCode) { + this(statusCode, 1); + } + + public StatusCount(Code statusCode, int count) { + this.statusCode = statusCode; + this.count = count; + } + + public Code getStatusCode() { + return statusCode; + } + + public int getCount() { + return count; + } + } + + private OpenTelemetryMetricsRecorder createOtelMetricsRecorder( + InMemoryMetricReader inMemoryMetricReader) { + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build(); + + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build(); + return new OpenTelemetryMetricsRecorder(openTelemetry, SERVICE_NAME); + } + + @Before + public void setup() throws Exception { + inMemoryMetricReader = InMemoryMetricReader.create(); + OpenTelemetryMetricsRecorder otelMetricsRecorder = + createOtelMetricsRecorder(inMemoryMetricReader); + grpcClient = + TestClientInitializer.createGrpcEchoClientOpentelemetry( + new MetricsTracerFactory(otelMetricsRecorder)); + httpClient = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry( + new MetricsTracerFactory(otelMetricsRecorder)); + } + + @After + public void cleanup() throws InterruptedException { + inMemoryMetricReader.shutdown(); + + grpcClient.close(); + httpClient.close(); + + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + httpClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + /** + * Iterate through all MetricData elements and check that the number of PointData values matches + * the expected value. A PointData element may have multiple values/ counts inside, so this + * extracts the value/ count from the PointData before summing. + * + *

The expected sum for an operation is `1`. Expected sum for an attempt may be 1+. + */ + private void verifyPointDataSum(List metricDataList, int attemptCount) { + for (MetricData metricData : metricDataList) { + Data data = metricData.getData(); + List points = new ArrayList<>(data.getPoints()); + switch (metricData.getName()) { + case OPERATION_COUNT: + long operationCountSum = + points.stream().map(x -> ((LongPointData) x).getValue()).reduce(0L, Long::sum); + Truth.assertThat(operationCountSum).isEqualTo(DEFAULT_OPERATION_COUNT); + break; + case ATTEMPT_COUNT: + long attemptCountSum = + points.stream().map(x -> ((LongPointData) x).getValue()).reduce(0L, Long::sum); + Truth.assertThat(attemptCountSum).isEqualTo(attemptCount); + break; + case OPERATION_LATENCY: + long operationLatencyCountSum = + points.stream().map(x -> ((HistogramPointData) x).getCount()).reduce(0L, Long::sum); + // It is difficult to verify the actual latency values (operation or attempt) + // without flaky behavior. Test that the number of data points recorded matches. + Truth.assertThat(operationLatencyCountSum).isEqualTo(DEFAULT_OPERATION_COUNT); + break; + case ATTEMPT_LATENCY: + long attemptLatencyCountSum = + points.stream().map(x -> ((HistogramPointData) x).getCount()).reduce(0L, Long::sum); + // It is difficult to verify the actual latency values (operation or attempt) + // without flaky behavior. Test that the number of data points recorded matches. + Truth.assertThat(attemptLatencyCountSum).isEqualTo(attemptCount); + break; + default: + break; + } + } + } + + /** + * Extract the attributes from MetricData and ensures that default attributes are recorded. Uses + * the `OPERATION_COUNT` MetricData to test the attributes. The `OPERATION_COUNT` is only recorded + * once and should only have one element of PointData. + * + *

Although the Status attribute is recorded by default on every operation, this helper method + * does not verify it. This is because every individual attempt (retry) may have a different + * status. {@link #verifyStatusAttribute(List, List)} is used to verify the statuses for every + * attempt. + */ + private void verifyDefaultMetricsAttributes( + List metricDataList, Map defaultAttributeMapping) { + Optional metricDataOptional = + metricDataList.stream().filter(x -> x.getName().equals(OPERATION_COUNT)).findAny(); + Truth.assertThat(metricDataOptional.isPresent()).isTrue(); + MetricData operationCountMetricData = metricDataOptional.get(); + + List pointDataList = new ArrayList<>(operationCountMetricData.getData().getPoints()); + // Operation Metrics should only have a 1 data point + Truth.assertThat(pointDataList.size()).isEqualTo(1); + Attributes recordedAttributes = pointDataList.get(0).getAttributes(); + Map, Object> recordedAttributesMap = recordedAttributes.asMap(); + for (Map.Entry entrySet : defaultAttributeMapping.entrySet()) { + String key = entrySet.getKey(); + String value = entrySet.getValue(); + AttributeKey stringAttributeKey = AttributeKey.stringKey(key); + Truth.assertThat(recordedAttributesMap.containsKey(stringAttributeKey)).isTrue(); + Truth.assertThat(recordedAttributesMap.get(stringAttributeKey)).isEqualTo(value); + } + } + + /** + * Extract the attributes from MetricData and ensures that default attributes are recorded. Uses + * the `ATTEMPT_COUNT` MetricData to test the attributes. The `ATTEMPT_COUNT` is recorded for + * every retry attempt and should record the status received that each attempt made. + */ + private void verifyStatusAttribute( + List metricDataList, List statusCountList) { + Optional metricDataOptional = + metricDataList.stream().filter(x -> x.getName().equals(ATTEMPT_COUNT)).findAny(); + Truth.assertThat(metricDataOptional.isPresent()).isTrue(); + MetricData attemptCountMetricData = metricDataOptional.get(); + + List pointDataList = new ArrayList<>(attemptCountMetricData.getData().getPoints()); + Truth.assertThat(pointDataList.size()).isEqualTo(statusCountList.size()); + + // The data for attempt count may not be ordered (i.e. the last data point recorded may be the + // first element in the PointData list). Search for the expected StatusCode from the + // statusCountList + // and match with the data inside the pointDataList + for (StatusCount statusCount : statusCountList) { + Code statusCode = statusCount.getStatusCode(); + Predicate pointDataPredicate = + x -> + x.getAttributes() + .get(AttributeKey.stringKey(MetricsTracer.STATUS_ATTRIBUTE)) + .equals(statusCode.toString()); + Optional pointDataOptional = + pointDataList.stream().filter(pointDataPredicate).findFirst(); + Truth.assertThat(pointDataOptional.isPresent()).isTrue(); + LongPointData longPointData = (LongPointData) pointDataOptional.get(); + Truth.assertThat(longPointData.getValue()).isEqualTo(statusCount.getCount()); + } + } + + /** + * Attempts to retrieve the metrics from the InMemoryMetricsReader. Sleep every second for at most + * 10s to try and retrieve all the metrics available. If it is unable to retrieve all the metrics, + * fail the test. + */ + private List getMetricDataList() throws InterruptedException { + for (int i = 0; i < NUM_COLLECTION_FLUSH_ATTEMPTS; i++) { + Thread.sleep(1000L); + List metricData = new ArrayList<>(inMemoryMetricReader.collectAllMetrics()); + if (metricData.size() == NUM_METRICS) { + return metricData; + } + } + Assert.fail("Unable to collect all the metrics required for the test"); + return new ArrayList<>(); + } + + @Test + public void testGrpc_operationSucceeded_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + EchoRequest echoRequest = + EchoRequest.newBuilder().setContent("test_grpc_operation_succeeded").build(); + grpcClient.echo(echoRequest); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Echo", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(Code.OK)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_operationSucceeded_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + EchoRequest echoRequest = + EchoRequest.newBuilder().setContent("test_http_operation_succeeded").build(); + httpClient.echo(echoRequest); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Echo", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(Code.OK)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Test + public void testGrpc_operationCancelled_recordsMetrics() throws Exception { + int attemptCount = 1; + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(5)) + .setSuccess(BlockResponse.newBuilder().setContent("grpc_operationCancelled")) + .build(); + + UnaryCallable blockCallable = grpcClient.blockCallable(); + ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); + // Sleep 1s before cancelling to let the request go through + Thread.sleep(1000); + blockResponseApiFuture.cancel(true); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(Code.CANCELLED)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_operationCancelled_recordsMetrics() throws Exception { + int attemptCount = 1; + BlockRequest blockRequest = + BlockRequest.newBuilder().setResponseDelay(Duration.newBuilder().setSeconds(5)).build(); + + UnaryCallable blockCallable = httpClient.blockCallable(); + ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); + // Sleep 1s before cancelling to let the request go through + Thread.sleep(1000); + blockResponseApiFuture.cancel(true); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(Code.CANCELLED)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Test + public void testGrpc_operationFailed_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + Code statusCode = Code.INVALID_ARGUMENT; + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(2)) + .setError(Status.newBuilder().setCode(statusCode.ordinal())) + .build(); + + UnaryCallable blockCallable = grpcClient.blockCallable(); + ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); + assertThrows(ExecutionException.class, blockResponseApiFuture::get); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_operationFailed_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + Code statusCode = Code.INVALID_ARGUMENT; + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(2)) + .setError(Status.newBuilder().setCode(statusCode.ordinal())) + .build(); + + UnaryCallable blockCallable = httpClient.blockCallable(); + ApiFuture blockResponseApiFuture = blockCallable.futureCall(blockRequest); + assertThrows(ExecutionException.class, blockResponseApiFuture::get); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Test + public void testGrpc_attemptFailedRetriesExhausted_recordsMetrics() throws Exception { + int attemptCount = 3; + Code 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. + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) + .setMaxAttempts(3) + .build(); + + EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder(); + grpcEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(ImmutableSet.of(statusCode)); + EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build()); + grpcEchoSettings = + grpcEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("localhost:7469") + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + grpcEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory( + new MetricsTracerFactory(createOtelMetricsRecorder(inMemoryMetricReader))) + .build(); + EchoStub stub = echoStubSettings.createStub(); + EchoClient grpcClient = EchoClient.create(stub); + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> grpcClient.echo(echoRequest)); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Echo", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode, 3)); + verifyStatusAttribute(metricDataList, statusCountList); + + grpcClient.close(); + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_attemptFailedRetriesExhausted_recordsMetrics() throws Exception { + int attemptCount = 3; + Code 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. + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(5000L)) + .setMaxAttempts(3) + .build(); + + EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder(); + httpJsonEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(ImmutableSet.of(statusCode)); + EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build()); + httpJsonEchoSettings = + httpJsonEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint("http://localhost:7469") + .build()) + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + httpJsonEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory( + new MetricsTracerFactory(createOtelMetricsRecorder(inMemoryMetricReader))) + .build(); + EchoStub stub = echoStubSettings.createStub(); + EchoClient httpClient = EchoClient.create(stub); + + EchoRequest echoRequest = + EchoRequest.newBuilder() + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(UnavailableException.class, () -> httpClient.echo(echoRequest)); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Echo", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode, 3)); + verifyStatusAttribute(metricDataList, statusCountList); + + httpClient.close(); + httpClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + @Test + public void testGrpc_attemptPermanentFailure_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + Code statusCode = Code.INVALID_ARGUMENT; + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(2).build()) + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(InvalidArgumentException.class, () -> grpcClient.block(blockRequest)); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_attemptPermanentFailure_recordsMetrics() throws InterruptedException { + int attemptCount = 1; + Code statusCode = Code.INVALID_ARGUMENT; + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(2).build()) + .setError(Status.newBuilder().setCode(statusCode.ordinal()).build()) + .build(); + + assertThrows(InvalidArgumentException.class, () -> httpClient.block(blockRequest)); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = ImmutableList.of(new StatusCount(statusCode)); + verifyStatusAttribute(metricDataList, statusCountList); + } + + @Test + public void testGrpc_multipleFailedAttempts_successfulOperation() throws Exception { + int attemptCount = 3; + // Disable Jitter on this test to try and ensure that the there are 3 attempts made + // for test. The first two calls should result in a DEADLINE_EXCEEDED exception as + // 0.5s and 1s are too short for the 1s blocking call (1s still requires time for + // the showcase server to respond back to the client). The 3rd and final call (2s) + // should result in an OK Status Code. + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setInitialRpcTimeout(org.threeten.bp.Duration.ofMillis(500L)) + .setRpcTimeoutMultiplier(2.0) + .setMaxRpcTimeout(org.threeten.bp.Duration.ofMillis(2000L)) + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(6000L)) + .setJittered(false) + .build(); + + EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder(); + grpcEchoSettingsBuilder + .blockSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(ImmutableSet.of(Code.DEADLINE_EXCEEDED)); + EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build()); + grpcEchoSettings = + grpcEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("localhost:7469") + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + grpcEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory( + new MetricsTracerFactory(createOtelMetricsRecorder(inMemoryMetricReader))) + .build(); + EchoStub stub = echoStubSettings.createStub(); + + EchoClient grpcClient = EchoClient.create(stub); + + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(1)) + .setSuccess(BlockResponse.newBuilder().setContent("grpcBlockResponse")) + .build(); + + grpcClient.block(blockRequest); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "Echo.Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + List statusCountList = + ImmutableList.of(new StatusCount(Code.DEADLINE_EXCEEDED, 2), new StatusCount(Code.OK)); + verifyStatusAttribute(metricDataList, statusCountList); + + grpcClient.close(); + grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + + @Ignore("https://github.com/googleapis/sdk-platform-java/issues/2503") + @Test + public void testHttpJson_multipleFailedAttempts_successfulOperation() throws Exception { + int attemptCount = 3; + RetrySettings retrySettings = + RetrySettings.newBuilder() + .setInitialRpcTimeout(org.threeten.bp.Duration.ofMillis(500L)) + .setRpcTimeoutMultiplier(2.0) + .setMaxRpcTimeout(org.threeten.bp.Duration.ofMillis(2000L)) + .setTotalTimeout(org.threeten.bp.Duration.ofMillis(6000L)) + .setJittered(false) + .build(); + + EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder(); + httpJsonEchoSettingsBuilder + .echoSettings() + .setRetrySettings(retrySettings) + .setRetryableCodes(ImmutableSet.of(Code.DEADLINE_EXCEEDED)); + EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build()); + httpJsonEchoSettings = + httpJsonEchoSettings + .toBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint("http://localhost:7469") + .build()) + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + httpJsonEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory( + new MetricsTracerFactory(createOtelMetricsRecorder(inMemoryMetricReader))) + .build(); + EchoStub stub = echoStubSettings.createStub(); + + EchoClient httpClient = EchoClient.create(stub); + + BlockRequest blockRequest = + BlockRequest.newBuilder() + .setResponseDelay(Duration.newBuilder().setSeconds(1)) + .setSuccess(BlockResponse.newBuilder().setContent("httpjsonBlockResponse")) + .build(); + + grpcClient.block(blockRequest); + + List metricDataList = getMetricDataList(); + verifyPointDataSum(metricDataList, attemptCount); + + Map attributeMapping = + ImmutableMap.of( + MetricsTracer.METHOD_NAME_ATTRIBUTE, + "google.showcase.v1beta1.Echo/Block", + MetricsTracer.LANGUAGE_ATTRIBUTE, + MetricsTracer.DEFAULT_LANGUAGE); + verifyDefaultMetricsAttributes(metricDataList, attributeMapping); + + httpClient.close(); + httpClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } +} diff --git a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index e620e254c6..f396a0fc6f 100644 --- a/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -24,6 +24,7 @@ import com.google.api.gax.retrying.RetrySettings; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.UnaryCallSettings; +import com.google.api.gax.tracing.ApiTracerFactory; import com.google.common.collect.ImmutableList; import com.google.showcase.v1beta1.ComplianceClient; import com.google.showcase.v1beta1.ComplianceSettings; @@ -32,6 +33,7 @@ import com.google.showcase.v1beta1.IdentityClient; import com.google.showcase.v1beta1.IdentitySettings; import com.google.showcase.v1beta1.WaitRequest; +import com.google.showcase.v1beta1.stub.EchoStub; import com.google.showcase.v1beta1.stub.EchoStubSettings; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; @@ -284,4 +286,55 @@ public static ComplianceClient createHttpJsonComplianceClient( .build(); return ComplianceClient.create(httpJsonComplianceSettings); } + + public static EchoClient createGrpcEchoClientOpentelemetry(ApiTracerFactory metricsTracerFactory) + throws Exception { + + EchoSettings grpcEchoSettings = + EchoSettings.newBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(ManagedChannelBuilder::usePlaintext) + .build()) + .setEndpoint("localhost:7469") + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + grpcEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory(metricsTracerFactory) + .build(); + EchoStub stub = echoStubSettings.createStub(); + + return EchoClient.create(stub); + } + + public static EchoClient createHttpJsonEchoClientOpentelemetry( + ApiTracerFactory metricsTracerFactory) throws Exception { + + EchoSettings httpJsonEchoSettings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport( + new NetHttpTransport.Builder().doNotValidateCertificate().build()) + .setEndpoint("http://localhost:7469") + .build()) + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + httpJsonEchoSettings + .getStubSettings() + .toBuilder() + .setTracerFactory(metricsTracerFactory) + .build(); + EchoStub stub = echoStubSettings.createStub(); + + return EchoClient.create(stub); + } } diff --git a/showcase/pom.xml b/showcase/pom.xml index c394812cac..7962851c15 100644 --- a/showcase/pom.xml +++ b/showcase/pom.xml @@ -53,7 +53,6 @@ gapic-showcase 0.0.1-SNAPSHOT - junit junit