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:
+ *
+ *
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