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