diff --git a/gax-java/gax/pom.xml b/gax-java/gax/pom.xml
index 57fb6f5799..950d4388f9 100644
--- a/gax-java/gax/pom.xml
+++ b/gax-java/gax/pom.xml
@@ -57,6 +57,18 @@
graal-sdk
provided
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+
+
+ io.opentelemetry
+ opentelemetry-exporter-otlp
+
diff --git a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
index 25929756f5..f986e2be81 100644
--- a/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
+++ b/gax-java/gax/src/main/java/com/google/api/gax/rpc/ClientSettings.java
@@ -33,6 +33,7 @@
import com.google.api.core.ApiFunction;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.ExecutorProvider;
+import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.common.base.MoreObjects;
import java.io.IOException;
import java.util.concurrent.Executor;
@@ -284,6 +285,16 @@ public B setGdchApiAudience(@Nullable String gdchApiAudience) {
return self();
}
+ /**
+ * Sets the ApiTracerFactory for the client instance. To enable default metrics, users need to
+ * create an instance of metricsRecorder and pass it to the metricsTracerFactory, and set it
+ * here.
+ */
+ public B setTracerFactory(@Nullable ApiTracerFactory tracerFactory) {
+ stubSettings.setTracerFactory(tracerFactory);
+ return self();
+ }
+
/**
* Gets the ExecutorProvider that was previously set on this Builder. This ExecutorProvider is
* to use for running asynchronous API call logic (such as retries and long-running operations),
@@ -351,6 +362,11 @@ public Duration getWatchdogCheckInterval() {
return stubSettings.getStreamWatchdogCheckInterval();
}
+ /** Gets the TracerFactory that was previously set in this Builder */
+ public ApiTracerFactory getTracerFactory() {
+ return stubSettings.getTracerFactory();
+ }
+
/** Gets the GDCH API audience that was previously set in this Builder */
@Nullable
public String getGdchApiAudience() {
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..af7b97664f 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
@@ -47,12 +47,14 @@
* 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";
+ private static final String LANGUAGE = "Java";
private Stopwatch attemptTimer;
@@ -64,6 +66,7 @@ public class MetricsTracer implements ApiTracer {
public MetricsTracer(MethodName methodName, MetricsRecorder metricsRecorder) {
this.attributes.put("method_name", methodName.toString());
+ this.attributes.put("language", LANGUAGE);
this.metricsRecorder = metricsRecorder;
}
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..3297c7a69c
--- /dev/null
+++ b/gax-java/gax/src/main/java/com/google/api/gax/tracing/OpentelemetryMetricsRecorder.java
@@ -0,0 +1,109 @@
+/*
+ * 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.common.annotations.VisibleForTesting;
+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;
+
+public class OpentelemetryMetricsRecorder implements MetricsRecorder {
+
+ private Meter meter;
+
+ private DoubleHistogram attemptLatencyRecorder;
+
+ private DoubleHistogram operationLatencyRecorder;
+
+ private LongCounter operationCountRecorder;
+
+ private LongCounter attemptCountRecorder;
+
+ public OpentelemetryMetricsRecorder(Meter meter) {
+ this.meter = meter;
+ this.attemptLatencyRecorder =
+ meter
+ .histogramBuilder("attempt_latency")
+ .setDescription("Duration of an individual attempt")
+ .setUnit("ms")
+ .build();
+ this.operationLatencyRecorder =
+ meter
+ .histogramBuilder("operation_latency")
+ .setDescription(
+ "Total time until final operation success or failure, including retries and backoff.")
+ .setUnit("ms")
+ .build();
+ this.operationCountRecorder =
+ meter
+ .counterBuilder("operation_count")
+ .setDescription("Count of Operations")
+ .setUnit("1")
+ .build();
+ this.attemptCountRecorder =
+ meter
+ .counterBuilder("attempt_count")
+ .setDescription("Count of Attempts")
+ .setUnit("1")
+ .build();
+ }
+
+ public void recordAttemptLatency(double attemptLatency, Map attributes) {
+ attemptLatencyRecorder.record(attemptLatency, toOtelAttributes(attributes));
+ }
+
+ public void recordAttemptCount(long count, Map attributes) {
+ attemptCountRecorder.add(count, toOtelAttributes(attributes));
+ }
+
+ public void recordOperationLatency(double operationLatency, Map attributes) {
+ operationLatencyRecorder.record(operationLatency, toOtelAttributes(attributes));
+ }
+
+ public void recordOperationCount(long count, Map attributes) {
+ operationCountRecorder.add(count, toOtelAttributes(attributes));
+ }
+
+ @VisibleForTesting
+ Attributes toOtelAttributes(Map attributes) {
+
+ if (attributes == null) {
+ throw new IllegalArgumentException("Input 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..e30d7f1662 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
@@ -77,7 +77,8 @@ public void testOperationSucceeded_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "OK",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordOperationCount(1, attributes);
verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes));
@@ -96,7 +97,8 @@ public void testOperationFailed_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "INVALID_ARGUMENT",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordOperationCount(1, attributes);
verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes));
@@ -112,7 +114,8 @@ public void testOperationCancelled_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "CANCELLED",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordOperationCount(1, attributes);
verify(metricsRecorder).recordOperationLatency(anyDouble(), eq(attributes));
@@ -132,7 +135,8 @@ public void testAttemptSucceeded_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "OK",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordAttemptCount(1, attributes);
verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes));
@@ -155,7 +159,8 @@ public void testAttemptFailed_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "INVALID_ARGUMENT",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordAttemptCount(1, attributes);
verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes));
@@ -174,7 +179,8 @@ public void testAttemptCancelled_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "CANCELLED",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordAttemptCount(1, attributes);
verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes));
@@ -196,7 +202,8 @@ public void testAttemptFailedRetriesExhausted_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "DEADLINE_EXCEEDED",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordAttemptCount(1, attributes);
verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes));
@@ -218,7 +225,8 @@ public void testAttemptPermanentFailure_recordsAttributes() {
Map attributes =
ImmutableMap.of(
"status", "NOT_FOUND",
- "method_name", "fake_service.fake_method");
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
verify(metricsRecorder).recordAttemptCount(1, attributes);
verify(metricsRecorder).recordAttemptLatency(anyDouble(), eq(attributes));
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..c14ccdb6f5
--- /dev/null
+++ b/gax-java/gax/src/test/java/com/google/api/gax/tracing/OpentelemetryMetricsRecorderTest.java
@@ -0,0 +1,237 @@
+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 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 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 {
+
+ // 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 Meter meter;
+ @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() {
+
+ meter = Mockito.mock(Meter.class);
+
+ // setup mocks for all the recorders using chained mocking
+ setupAttemptCountRecorder();
+ setupAttemptLatencyRecorder();
+ setupOperationLatencyRecorder();
+ setupOperationCountRecorder();
+
+ otelMetricsRecorder = new OpentelemetryMetricsRecorder(meter);
+ }
+
+ @Test
+ public void testAttemptCountRecorder_recordsAttributes() {
+
+ ImmutableMap attributes =
+ ImmutableMap.of(
+ "status", "OK",
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
+
+ Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+ otelMetricsRecorder.recordAttemptCount(1, attributes);
+
+ verify(attemptCountRecorder).add(1, otelAttributes);
+ verifyNoMoreInteractions(attemptCountRecorder);
+ }
+
+ @Test
+ public void testAttemptLatencyRecorder_recordsAttributes() {
+
+ ImmutableMap attributes =
+ ImmutableMap.of(
+ "status", "NOT_FOUND",
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
+
+ Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+ otelMetricsRecorder.recordAttemptLatency(1.1, attributes);
+
+ verify(attemptLatencyRecorder).record(1.1, otelAttributes);
+ verifyNoMoreInteractions(attemptLatencyRecorder);
+ }
+
+ @Test
+ public void testOperationCountRecorder_recordsAttributes() {
+
+ ImmutableMap attributes =
+ ImmutableMap.of(
+ "status", "OK",
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
+
+ Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+ otelMetricsRecorder.recordOperationCount(1, attributes);
+
+ verify(operationCountRecorder).add(1, otelAttributes);
+ verifyNoMoreInteractions(operationCountRecorder);
+ }
+
+ @Test
+ public void testOperationLatencyRecorder_recordsAttributes() {
+
+ ImmutableMap attributes =
+ ImmutableMap.of(
+ "status", "INVALID_ARGUMENT",
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
+
+ Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+ otelMetricsRecorder.recordOperationLatency(1.7, attributes);
+
+ verify(operationLatencyRecorder).record(1.7, otelAttributes);
+ verifyNoMoreInteractions(operationLatencyRecorder);
+ }
+
+ @Test
+ public void testToOtelAttributes_correctConversion() {
+
+ ImmutableMap attributes =
+ ImmutableMap.of(
+ "status", "OK",
+ "method_name", "fake_service.fake_method",
+ "language", "Java");
+
+ Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+
+ Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("status"))).isEqualTo("OK");
+ Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("method_name")))
+ .isEqualTo("fake_service.fake_method");
+ Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("language"))).isEqualTo("Java");
+ }
+
+ @Test
+ public void testToOtelAttributes_nullInput() {
+
+ Throwable thrown =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> {
+ otelMetricsRecorder.toOtelAttributes(null);
+ });
+ Truth.assertThat(thrown).hasMessageThat().contains("Input attributes map cannot be null");
+ }
+
+ /// this is a potential candidate for test in the future when we enforce non-null values in the
+ // attributes map
+ // will remove this before merging the PR
+ // @Test
+ // public void testToOtelAttributes_nullKeyValuePair() {
+ //
+ //
+ // Map attributes = new HashMap<>();
+ // attributes.put("status", "OK");
+ // attributes.put("method_name", "fake_service.fake_method");
+ // attributes.put("language", "Java");
+ // // try to insert a key-value pair with a null value
+ // attributes.put("fakeDatabaseId", null);
+ //
+ // Attributes otelAttributes = otelMetricsRecorder.toOtelAttributes(attributes);
+ //
+ // //only 3 attributes should be added to the Attributes object
+ // Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("status"))).isEqualTo("OK");
+ //
+ // Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("method_name"))).isEqualTo("fake_service.fake_method");
+ // Truth.assertThat(otelAttributes.get(AttributeKey.stringKey("language"))).isEqualTo("Java");
+ //
+ // // attributes should only have 3 entries since the 4th attribute value is null
+ // Truth.assertThat(otelAttributes.size()).isEqualTo(3);
+ // }
+
+ private void setupAttemptCountRecorder() {
+
+ attemptCountRecorderBuilder = Mockito.mock(LongCounterBuilder.class);
+ attemptCountRecorder = Mockito.mock(LongCounter.class);
+
+ // Configure chained mocking for AttemptCountRecorder
+ Mockito.when(meter.counterBuilder("attempt_count")).thenReturn(attemptCountRecorderBuilder);
+ Mockito.when(attemptCountRecorderBuilder.setDescription("Count of Attempts"))
+ .thenReturn(attemptCountRecorderBuilder);
+ Mockito.when(attemptCountRecorderBuilder.setUnit("1")).thenReturn(attemptCountRecorderBuilder);
+ Mockito.when(attemptCountRecorderBuilder.build()).thenReturn(attemptCountRecorder);
+ }
+
+ private void setupOperationCountRecorder() {
+
+ operationCountRecorderBuilder = Mockito.mock(LongCounterBuilder.class);
+ operationCountRecorder = Mockito.mock(LongCounter.class);
+
+ // Configure chained mocking for operationCountRecorder
+ Mockito.when(meter.counterBuilder("operation_count")).thenReturn(operationCountRecorderBuilder);
+ Mockito.when(operationCountRecorderBuilder.setDescription("Count of Operations"))
+ .thenReturn(operationCountRecorderBuilder);
+ Mockito.when(operationCountRecorderBuilder.setUnit("1"))
+ .thenReturn(operationCountRecorderBuilder);
+ Mockito.when(operationCountRecorderBuilder.build()).thenReturn(operationCountRecorder);
+ }
+
+ private void setupAttemptLatencyRecorder() {
+
+ attemptLatencyRecorderBuilder = Mockito.mock(DoubleHistogramBuilder.class);
+ attemptLatencyRecorder = Mockito.mock(DoubleHistogram.class);
+
+ // Configure chained mocking for attemptLatencyRecorder
+ Mockito.when(meter.histogramBuilder("attempt_latency"))
+ .thenReturn(attemptLatencyRecorderBuilder);
+ Mockito.when(attemptLatencyRecorderBuilder.setDescription("Duration of an individual attempt"))
+ .thenReturn(attemptLatencyRecorderBuilder);
+ Mockito.when(attemptLatencyRecorderBuilder.setUnit("ms"))
+ .thenReturn(attemptLatencyRecorderBuilder);
+ Mockito.when(attemptLatencyRecorderBuilder.build()).thenReturn(attemptLatencyRecorder);
+ }
+
+ private void setupOperationLatencyRecorder() {
+
+ operationLatencyRecorderBuilder = Mockito.mock(DoubleHistogramBuilder.class);
+ operationLatencyRecorder = Mockito.mock(DoubleHistogram.class);
+
+ // Configure chained mocking for operationLatencyRecorder
+ Mockito.when(meter.histogramBuilder("operation_latency"))
+ .thenReturn(operationLatencyRecorderBuilder);
+ Mockito.when(
+ operationLatencyRecorderBuilder.setDescription(
+ "Total time until final operation success or failure, including retries and backoff."))
+ .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 5e98463187..4ebfbd34a0 100644
--- a/gax-java/pom.xml
+++ b/gax-java/pom.xml
@@ -158,6 +158,13 @@
pom
import
+
+ io.opentelemetry
+ opentelemetry-bom
+ 1.27.0
+ pom
+ import
+
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..214521a7bc
--- /dev/null
+++ b/showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelMetrics.java
@@ -0,0 +1,449 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.showcase.v1beta1.it;
+
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.gax.core.GaxProperties;
+import com.google.api.gax.core.NoCredentialsProvider;
+import com.google.api.gax.retrying.RetrySettings;
+import com.google.api.gax.rpc.StatusCode.Code;
+import com.google.api.gax.tracing.MetricsTracerFactory;
+import com.google.api.gax.tracing.OpentelemetryMetricsRecorder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.truth.Truth;
+import com.google.rpc.Status;
+import com.google.showcase.v1beta1.BlockRequest;
+import com.google.showcase.v1beta1.EchoClient;
+import com.google.showcase.v1beta1.EchoRequest;
+import com.google.showcase.v1beta1.EchoSettings;
+import com.google.showcase.v1beta1.it.util.TestClientInitializer;
+import com.google.showcase.v1beta1.stub.EchoStubSettings;
+import io.grpc.ManagedChannelBuilder;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.HistogramData;
+import io.opentelemetry.sdk.metrics.data.HistogramPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.resources.Resource;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricExporter;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+public class ITOtelMetrics {
+
+ @Test
+ public void testHttpJson_OperationSucceded_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ EchoClient client =
+ TestClientInitializer.createHttpJsonEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ client.echo(EchoRequest.newBuilder().setContent("test_http_operation_succeeded").build());
+
+ Thread.sleep(1000);
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("google.showcase.v1beta1.Echo/Echo");
+ Truth.assertThat(status).isEqualTo("OK");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGrpc_OperationSucceded_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ InMemoryMetricExporter exporter = InMemoryMetricExporter.create();
+
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ EchoClient client =
+ TestClientInitializer.createGrpcEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ client.echo(EchoRequest.newBuilder().setContent("test_grpc_operation_succeeded").build());
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("Echo.Echo");
+ Truth.assertThat(status).isEqualTo("OK");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testHttpJson_OperationCancelled_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ EchoClient client =
+ TestClientInitializer.createHttpJsonEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ client
+ .echoCallable()
+ .futureCall(EchoRequest.newBuilder().setContent("test_http_operation_cancelled").build())
+ .cancel(true);
+
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("google.showcase.v1beta1.Echo/Echo");
+ Truth.assertThat(status).isEqualTo("CANCELLED");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGrpc_OperationCancelled_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+ EchoClient client =
+ TestClientInitializer.createGrpcEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ EchoRequest requestWithNoError =
+ EchoRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.OK.ordinal()).build())
+ .build();
+
+ client.echoCallable().futureCall(requestWithNoError).cancel(true);
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("Echo.Echo");
+ Truth.assertThat(status).isEqualTo("CANCELLED");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testHttpJson_OperationFailed_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+ EchoClient client =
+ TestClientInitializer.createHttpJsonEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ EchoRequest requestWithNoError =
+ EchoRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.UNKNOWN.ordinal()).build())
+ .build();
+
+ client.echoCallable().futureCall(requestWithNoError);
+ Thread.sleep(1000);
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("google.showcase.v1beta1.Echo/Echo");
+ Truth.assertThat(status).isEqualTo("UNKNOWN");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGrpc_OperationFailed_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+ EchoClient client =
+ TestClientInitializer.createGrpcEchoClientOpentelemetry(
+ new MetricsTracerFactory(otelMetricsRecorder));
+
+ EchoRequest requestWithNoError =
+ EchoRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.INVALID_ARGUMENT.ordinal()).build())
+ .build();
+
+ client.echoCallable().futureCall(requestWithNoError);
+ Thread.sleep(1000);
+
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("Echo.Echo");
+ Truth.assertThat(status).isEqualTo("INVALID_ARGUMENT");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGrpc_attemptFailedRetriesExhausted_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ RetrySettings retrySettings = RetrySettings.newBuilder().setMaxAttempts(7).build();
+
+ EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder();
+ grpcEchoSettingsBuilder
+ .blockSettings()
+ .setRetrySettings(retrySettings)
+ .setRetryableCodes(ImmutableSet.of(Code.INVALID_ARGUMENT));
+
+ EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build());
+ grpcEchoSettings =
+ grpcEchoSettings
+ .toBuilder()
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .setTracerFactory(new MetricsTracerFactory(otelMetricsRecorder))
+ .setTransportChannelProvider(
+ EchoSettings.defaultGrpcTransportProviderBuilder()
+ .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
+ .build())
+ .setEndpoint("localhost:7469")
+ .build();
+
+ EchoClient grpcClientWithRetrySetting = EchoClient.create(grpcEchoSettings);
+
+ BlockRequest blockRequest =
+ BlockRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.INVALID_ARGUMENT.ordinal()).build())
+ .build();
+
+ grpcClientWithRetrySetting.blockCallable().futureCall(blockRequest).isDone();
+
+ Thread.sleep(1000);
+
+ inMemoryMetricReader.flush();
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+
+ System.out.println(pointData);
+
+ // add a comment why I am doing this
+ double max = pointData.getMax();
+ double min = pointData.getMin();
+ if (max != min) {
+ Truth.assertThat(pointData.getCount()).isEqualTo(7);
+ }
+ Truth.assertThat(method).isEqualTo("Echo.Block");
+ Truth.assertThat(status).isEqualTo("INVALID_ARGUMENT");
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testHttpjson_attemptFailedRetriesExhausted_recordsMetrics() throws Exception {
+
+ RetrySettings retrySettings = RetrySettings.newBuilder().setMaxAttempts(5).build();
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ EchoStubSettings.Builder httpJsonEchoSettingsBuilder = EchoStubSettings.newHttpJsonBuilder();
+ httpJsonEchoSettingsBuilder
+ .blockSettings()
+ .setRetrySettings(retrySettings)
+ .setRetryableCodes(ImmutableSet.of(Code.INVALID_ARGUMENT));
+
+ EchoSettings httpJsonEchoSettings = EchoSettings.create(httpJsonEchoSettingsBuilder.build());
+ httpJsonEchoSettings =
+ httpJsonEchoSettings
+ .toBuilder()
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .setTracerFactory(new MetricsTracerFactory(otelMetricsRecorder))
+ .setTransportChannelProvider(
+ EchoSettings.defaultHttpJsonTransportProviderBuilder()
+ .setHttpTransport(
+ new NetHttpTransport.Builder().doNotValidateCertificate().build())
+ .setEndpoint("http://localhost:7469")
+ .build())
+ .build();
+
+ EchoClient httpJsonClientWithRetrySetting = EchoClient.create(httpJsonEchoSettings);
+
+ BlockRequest blockRequest =
+ BlockRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.INVALID_ARGUMENT.ordinal()).build())
+ .build();
+
+ httpJsonClientWithRetrySetting.blockCallable().futureCall(blockRequest);
+
+ Thread.sleep(1000);
+ inMemoryMetricReader.flush();
+
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String language = pointData.getAttributes().get(AttributeKey.stringKey("language"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("google.showcase.v1beta1.Echo/Block");
+ Truth.assertThat(language).isEqualTo("Java");
+ Truth.assertThat(status).isEqualTo("UNKNOWN");
+ Truth.assertThat(pointData.getCount()).isEqualTo(5);
+ }
+ }
+ }
+ }
+
+ @Test
+ public void testGrpc_attemptPermanentFailure_recordsMetrics() throws Exception {
+
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ OpentelemetryMetricsRecorder otelMetricsRecorder =
+ createOtelMetricsRecorder(inMemoryMetricReader);
+
+ RetrySettings retrySettings = RetrySettings.newBuilder().setMaxAttempts(3).build();
+
+ EchoStubSettings.Builder grpcEchoSettingsBuilder = EchoStubSettings.newBuilder();
+ grpcEchoSettingsBuilder
+ .blockSettings()
+ .setRetrySettings(retrySettings)
+ .setRetryableCodes(ImmutableSet.of(Code.PERMISSION_DENIED));
+
+ EchoSettings grpcEchoSettings = EchoSettings.create(grpcEchoSettingsBuilder.build());
+ grpcEchoSettings =
+ grpcEchoSettings
+ .toBuilder()
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .setTracerFactory(new MetricsTracerFactory(otelMetricsRecorder))
+ .setTransportChannelProvider(
+ EchoSettings.defaultGrpcTransportProviderBuilder()
+ .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
+ .build())
+ .setEndpoint("localhost:7469")
+ .build();
+
+ EchoClient grpcClientWithRetrySetting = EchoClient.create(grpcEchoSettings);
+
+ BlockRequest blockRequest =
+ BlockRequest.newBuilder()
+ .setError(Status.newBuilder().setCode(Code.INVALID_ARGUMENT.ordinal()).build())
+ .build();
+
+ grpcClientWithRetrySetting.blockCallable().futureCall(blockRequest).isDone();
+
+ Thread.sleep(1000);
+ inMemoryMetricReader.flush();
+
+ List metricDataList = new ArrayList<>(inMemoryMetricReader.collectAllMetrics());
+
+ for (MetricData metricData : metricDataList) {
+ HistogramData histogramData = metricData.getHistogramData();
+ if (!histogramData.getPoints().isEmpty()) {
+ for (HistogramPointData pointData : histogramData.getPoints()) {
+ String method = pointData.getAttributes().get(AttributeKey.stringKey("method_name"));
+ String language = pointData.getAttributes().get(AttributeKey.stringKey("language"));
+ String status = pointData.getAttributes().get(AttributeKey.stringKey("status"));
+ Truth.assertThat(method).isEqualTo("Echo.Block");
+ Truth.assertThat(language).isEqualTo("Java");
+ Truth.assertThat(status).isEqualTo("INVALID_ARGUMENT");
+ Truth.assertThat(pointData.getCount()).isEqualTo(1);
+ }
+ }
+ }
+ }
+
+ private OpentelemetryMetricsRecorder createOtelMetricsRecorder(
+ InMemoryMetricReader inMemoryMetricReader) {
+
+ Resource resource = Resource.getDefault();
+ SdkMeterProvider sdkMeterProvider =
+ SdkMeterProvider.builder()
+ .setResource(resource)
+ .registerMetricReader(inMemoryMetricReader)
+ .build();
+
+ OpenTelemetry openTelemetry =
+ OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build();
+
+ // Meter Creation
+ Meter meter =
+ openTelemetry
+ .meterBuilder("gax")
+ .setInstrumentationVersion(GaxProperties.getGaxVersion())
+ .build();
+ // OpenTelemetry Metrics Recorder
+ return new OpentelemetryMetricsRecorder(meter);
+ }
+}
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..ff3fb0c82d 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;
@@ -284,4 +285,39 @@ public static ComplianceClient createHttpJsonComplianceClient(
.build();
return ComplianceClient.create(httpJsonComplianceSettings);
}
+
+ public static EchoClient createHttpJsonEchoClientOpentelemetry(
+ ApiTracerFactory metricsTracerFactory) throws Exception {
+
+ EchoSettings httpJsonEchoSettings =
+ EchoSettings.newHttpJsonBuilder()
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .setTracerFactory(metricsTracerFactory)
+ .setTransportChannelProvider(
+ EchoSettings.defaultHttpJsonTransportProviderBuilder()
+ .setHttpTransport(
+ new NetHttpTransport.Builder().doNotValidateCertificate().build())
+ .setEndpoint("http://localhost:7469")
+ .build())
+ .build();
+
+ return EchoClient.create(httpJsonEchoSettings);
+ }
+
+ public static EchoClient createGrpcEchoClientOpentelemetry(ApiTracerFactory metricsTracerFactory)
+ throws Exception {
+
+ EchoSettings grpcEchoSettings =
+ EchoSettings.newBuilder()
+ .setCredentialsProvider(NoCredentialsProvider.create())
+ .setTracerFactory(metricsTracerFactory)
+ .setTransportChannelProvider(
+ EchoSettings.defaultGrpcTransportProviderBuilder()
+ .setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
+ .build())
+ .setEndpoint("localhost:7469")
+ .build();
+
+ return EchoClient.create(grpcEchoSettings);
+ }
}
diff --git a/showcase/pom.xml b/showcase/pom.xml
index 13fbf1e1ea..9651f01eb9 100644
--- a/showcase/pom.xml
+++ b/showcase/pom.xml
@@ -38,6 +38,13 @@
pom
import
+
+ io.opentelemetry
+ opentelemetry-bom
+ 1.27.0
+ pom
+ import
+
com.google.api.grpc
proto-gapic-showcase-v1beta1
@@ -53,7 +60,6 @@
gapic-showcase
0.0.1-SNAPSHOT
-
junit
junit
@@ -63,6 +69,21 @@
+
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+
+
+ io.opentelemetry
+ opentelemetry-sdk-metrics-testing
+
+
+
gapic-showcase
grpc-gapic-showcase-v1beta1