diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java index 299a2cdb3316..19fec5879cf1 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorder.java @@ -29,12 +29,16 @@ */ package com.google.api.gax.tracing; +import com.google.api.gax.rpc.LibraryMetadata; +import com.google.common.base.Strings; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterBuilder; import java.util.Arrays; import java.util.List; import java.util.Map; +import javax.annotation.Nullable; /** * This class takes an OpenTelemetry object, and creates instruments (meters, histograms etc.) from @@ -53,8 +57,23 @@ class GoldenSignalsMetricsRecorder { 900.0, 3600.0); final DoubleHistogram clientRequestDurationRecorder; - GoldenSignalsMetricsRecorder(OpenTelemetry openTelemetry, String libraryName) { - Meter meter = openTelemetry.meterBuilder(libraryName).build(); + @Nullable + static GoldenSignalsMetricsRecorder create( + OpenTelemetry openTelemetry, LibraryMetadata libraryMetadata) { + if (libraryMetadata == null || Strings.isNullOrEmpty(libraryMetadata.artifactName())) { + return null; + } + return new GoldenSignalsMetricsRecorder(openTelemetry, libraryMetadata); + } + + private GoldenSignalsMetricsRecorder( + OpenTelemetry openTelemetry, LibraryMetadata libraryMetadata) { + MeterBuilder meterBuilder = openTelemetry.meterBuilder(libraryMetadata.artifactName()); + String libraryVersion = libraryMetadata.version(); + if (!Strings.isNullOrEmpty(libraryVersion)) { + meterBuilder.setInstrumentationVersion(libraryVersion); + } + Meter meter = meterBuilder.build(); this.clientRequestDurationRecorder = meter diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java index 875947661e8f..f24bed478ee7 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracer.java @@ -29,14 +29,12 @@ */ package com.google.api.gax.tracing; -import static com.google.api.gax.tracing.ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE; - -import com.google.api.gax.rpc.StatusCode; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.TimeUnit; /** @@ -77,21 +75,25 @@ class GoldenSignalsMetricsTracer implements ApiTracer { */ @Override public void operationSucceeded() { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.OK.toString()); + ObservabilityUtils.populateStatusAttributes(attributes, null, transport); metricsRecorder.recordOperationLatency( clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); } @Override public void operationCancelled() { - attributes.put(RPC_RESPONSE_STATUS_ATTRIBUTE, StatusCode.Code.CANCELLED.toString()); - metricsRecorder.recordOperationLatency( - clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); + recordError(new CancellationException()); } @Override public void operationFailed(Throwable error) { + recordError(error); + } + + private void recordError(Throwable error) { ObservabilityUtils.populateStatusAttributes(attributes, error, transport); + attributes.put( + ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE, ObservabilityUtils.extractErrorType(error)); metricsRecorder.recordOperationLatency( clientRequestTimer.elapsed(TimeUnit.NANOSECONDS) / 1_000_000_000.0, attributes); } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java index 34da82de09d8..3857cf59b0c7 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerFactory.java @@ -29,10 +29,8 @@ */ package com.google.api.gax.tracing; -import com.google.api.client.util.Strings; import com.google.api.core.BetaApi; import com.google.api.core.InternalApi; -import com.google.api.gax.rpc.LibraryMetadata; import io.opentelemetry.api.OpenTelemetry; /** @@ -85,14 +83,12 @@ public ApiTracerFactory withContext(ApiTracerContext context) { if (context == null) { return new BaseApiTracerFactory(); } - LibraryMetadata metadata = context.libraryMetadata(); - if (metadata == null || metadata.isEmpty() || Strings.isNullOrEmpty(metadata.artifactName())) { + this.metricsRecorder = + GoldenSignalsMetricsRecorder.create(openTelemetry, context.libraryMetadata()); + if (this.metricsRecorder == null) { return new BaseApiTracerFactory(); } this.clientLevelTracerContext = context; - this.metricsRecorder = - new GoldenSignalsMetricsRecorder( - openTelemetry, clientLevelTracerContext.libraryMetadata().artifactName()); return this; } } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java index d3957530c544..f538d0807e78 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/ObservabilityUtils.java @@ -175,11 +175,10 @@ static void populateStatusAttributes( @Nullable Throwable error, ApiTracerContext.Transport transport) { StatusCode.Code code = extractStatus(error); + attributes.put(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, code.toString()); if (transport == ApiTracerContext.Transport.HTTP) { attributes.put( ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE, (long) code.getHttpStatusCode()); - } else { - attributes.put(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE, code.toString()); } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java index 3476e64a723d..51191671a65d 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsRecorderTest.java @@ -34,6 +34,7 @@ import static com.google.api.gax.tracing.GoldenSignalsMetricsRecorder.CLIENT_REQUEST_DURATION_METRIC_NAME; import static com.google.common.truth.Truth.assertThat; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; @@ -62,7 +63,13 @@ void setUp() { SdkMeterProvider.builder().registerMetricReader(metricReader).build(); OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); - recorder = new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME); + recorder = + GoldenSignalsMetricsRecorder.create( + openTelemetry, + LibraryMetadata.newBuilder() + .setArtifactName(ARTIFACT_NAME) + .setVersion("1.2.3") + .build()); } @Test @@ -114,4 +121,27 @@ void recordOperationLatency_shouldRecordMetricAttributes() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo(Attributes.of(AttributeKey.stringKey(ATTRIBUTE_1), VALUE_1)); } + + @Test + void create_shouldReturnNull_whenLibraryMetadataIsNull() { + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), null); + assertThat(actual).isNull(); + } + + @Test + void create_shouldReturnNull_whenArtifactNameIsNull() { + LibraryMetadata metadata = LibraryMetadata.newBuilder().setVersion("1.0.0").build(); + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), metadata); + assertThat(actual).isNull(); + } + + @Test + void create_shouldReturnNull_whenArtifactNameIsEmpty() { + LibraryMetadata metadata = LibraryMetadata.newBuilder().setArtifactName("").build(); + GoldenSignalsMetricsRecorder actual = + GoldenSignalsMetricsRecorder.create(OpenTelemetry.noop(), metadata); + assertThat(actual).isNull(); + } } diff --git a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java index 1d70445a3e81..5a4377300765 100644 --- a/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java +++ b/sdk-platform-java/gax-java/gax/src/test/java/com/google/api/gax/tracing/GoldenSignalsMetricsTracerTest.java @@ -33,6 +33,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.LibraryMetadata; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.testing.FakeStatusCode; import com.google.common.testing.FakeTicker; @@ -68,7 +69,12 @@ void setUp() { ticker = new FakeTicker(); tracer = new GoldenSignalsMetricsTracer( - new GoldenSignalsMetricsRecorder(openTelemetry, ARTIFACT_NAME), + GoldenSignalsMetricsRecorder.create( + openTelemetry, + LibraryMetadata.newBuilder() + .setArtifactName(ARTIFACT_NAME) + .setVersion("1.2.3") + .build()), ApiTracerContext.empty(), ticker); } @@ -129,6 +135,8 @@ void operationCancelled_shouldRecordsOKStatus() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo( Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CancellationException", AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), StatusCode.Code.CANCELLED.toString())); } @@ -163,7 +171,67 @@ void operationFailed_shouldRecordsOKStatus() { assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) .isEqualTo( Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "INTERNAL", AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), StatusCode.Code.INTERNAL.toString())); } + + @Test + void operationFailed_shouldRecordCancellationException() { + java.util.concurrent.CancellationException error = + new java.util.concurrent.CancellationException("test cancellation"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CancellationException", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.CANCELLED.toString())); + } + + @Test + void operationFailed_shouldRecordClientTimeout() { + java.net.SocketTimeoutException error = new java.net.SocketTimeoutException("test timeout"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CLIENT_TIMEOUT", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.UNKNOWN.toString())); + } + + @Test + void operationFailed_shouldRecordClientRequestError() { + IllegalArgumentException error = new IllegalArgumentException("test illegal argument"); + tracer.operationFailed(error); + + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).hasSize(1); + MetricData metricData = metrics.iterator().next(); + + assertThat(metricData.getHistogramData().getPoints()).hasSize(1); + assertThat(metricData.getHistogramData().getPoints().iterator().next().getAttributes()) + .isEqualTo( + Attributes.of( + AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE), + "CLIENT_REQUEST_ERROR", + AttributeKey.stringKey(RPC_RESPONSE_STATUS_ATTRIBUTE), + StatusCode.Code.UNKNOWN.toString())); + } } diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java new file mode 100644 index 000000000000..70f86141f6c5 --- /dev/null +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITOtelGoldenMetrics.java @@ -0,0 +1,382 @@ +/* + * Copyright 2026 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.showcase.v1beta1.it; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.api.client.http.HttpTransport; +import com.google.api.gax.core.NoCredentialsProvider; +import com.google.api.gax.rpc.StatusCode; +import com.google.api.gax.rpc.TransportChannelProvider; +import com.google.api.gax.rpc.UnavailableException; +import com.google.api.gax.tracing.GoldenSignalsMetricsTracerFactory; +import com.google.api.gax.tracing.ObservabilityAttributes; +import com.google.common.collect.ImmutableList; +import com.google.rpc.Status; +import com.google.showcase.v1beta1.EchoClient; +import com.google.showcase.v1beta1.EchoRequest; +import com.google.showcase.v1beta1.EchoResponse; +import com.google.showcase.v1beta1.EchoSettings; +import com.google.showcase.v1beta1.it.util.TestClientInitializer; +import com.google.showcase.v1beta1.stub.EchoStubSettings; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.Metadata; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Collection; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ITOtelGoldenMetrics { + private static final String SHOWCASE_SERVER_ADDRESS = "localhost"; + private static final long SHOWCASE_SERVER_PORT = 7469; + private static final String SHOWCASE_ARTIFACT = "com.google.cloud:gapic-showcase"; + + private InMemoryMetricReader metricReader; + private OpenTelemetrySdk openTelemetrySdk; + + @BeforeEach + void setup() { + metricReader = InMemoryMetricReader.create(); + + SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader).build(); + + openTelemetrySdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); + } + + @AfterEach + void tearDown() { + if (openTelemetrySdk != null) { + openTelemetrySdk.close(); + } + } + + @Test + void testMetrics_successfulEcho_grpc() throws Exception { + GoldenSignalsMetricsTracerFactory tracerFactory = + new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry(tracerFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()); + + // The end of an operation is tracked in a separate thread. + // Add a small sleep to make sure the tracking is completed. + // This is implemented by adding a TraceFinisher to ApiFuture as a callback in TracedUnaryCallable, + // which could be executed in a different thread. + Thread.sleep(100); + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isNotEmpty(); + + MetricData durationMetric = + metrics.stream() + .filter(m -> m.getName().equals("gcp.client.request.duration")) + .findFirst() + .orElseThrow(() -> new AssertionError("Duration metric not found")); + + assertThat(durationMetric.getInstrumentationScopeInfo().getName()) + .isEqualTo(SHOWCASE_ARTIFACT); + + io.opentelemetry.api.common.Attributes attributes = + durationMetric.getHistogramData().getPoints().iterator().next().getAttributes(); + + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_PORT); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE))) + .isEqualTo("grpc"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE))) + .isEqualTo("showcase"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE))) + .isEqualTo("google.showcase.v1beta1.Echo/Echo"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("OK"); + } + } + + @Test + void testMetrics_failedEcho_grpc_recordsErrorType() throws Exception { + GoldenSignalsMetricsTracerFactory tracerFactory = + new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); + + ClientInterceptor interceptor = + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new ClientCall() { + @Override + public void start(Listener responseListener, Metadata headers) { + responseListener.onClose(io.grpc.Status.UNAVAILABLE, new Metadata()); + } + + @Override + public void request(int numMessages) {} + + @Override + public void cancel(String message, Throwable cause) {} + + @Override + public void halfClose() {} + + @Override + public void sendMessage(ReqT message) {} + }; + } + }; + + TransportChannelProvider transportChannelProvider = + EchoSettings.defaultGrpcTransportProviderBuilder() + .setChannelConfigurator(io.grpc.ManagedChannelBuilder::usePlaintext) + .setInterceptorProvider(() -> ImmutableList.of(interceptor)) + .build(); + + try (EchoClient client = + TestClientInitializer.createGrpcEchoClientOpentelemetry( + tracerFactory, transportChannelProvider)) { + + assertThrows( + UnavailableException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build())); + + Thread.sleep(100); + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isNotEmpty(); + + MetricData durationMetric = + metrics.stream() + .filter(m -> m.getName().equals("gcp.client.request.duration")) + .findFirst() + .orElseThrow(() -> new AssertionError("Duration metric not found")); + + io.opentelemetry.api.common.Attributes attributes = + durationMetric.getHistogramData().getPoints().iterator().next().getAttributes(); + + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("UNAVAILABLE"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo("UNAVAILABLE"); + } + } + + @Test + void testMetrics_successfulEcho_httpjson() throws Exception { + GoldenSignalsMetricsTracerFactory tracerFactory = + new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); + + try (EchoClient client = + TestClientInitializer.createHttpJsonEchoClientOpentelemetry(tracerFactory)) { + + client.echo(EchoRequest.newBuilder().setContent("metrics-test").build()); + + Thread.sleep(100); + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isNotEmpty(); + + MetricData durationMetric = + metrics.stream() + .filter(m -> m.getName().equals("gcp.client.request.duration")) + .findFirst() + .orElseThrow(() -> new AssertionError("Duration metric not found")); + + assertThat(durationMetric.getInstrumentationScopeInfo().getName()) + .isEqualTo(SHOWCASE_ARTIFACT); + + io.opentelemetry.api.common.Attributes attributes = + durationMetric.getHistogramData().getPoints().iterator().next().getAttributes(); + + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.SERVER_ADDRESS_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_ADDRESS); + assertThat( + attributes.get(AttributeKey.longKey(ObservabilityAttributes.SERVER_PORT_ATTRIBUTE))) + .isEqualTo(SHOWCASE_SERVER_PORT); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_SYSTEM_NAME_ATTRIBUTE))) + .isEqualTo("http"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GCP_CLIENT_SERVICE_ATTRIBUTE))) + .isEqualTo("showcase"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("OK"); + assertThat( + attributes.get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo(200L); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.URL_TEMPLATE_ATTRIBUTE))) + .isEqualTo("v1beta1/echo:echo"); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.GRPC_RPC_METHOD_ATTRIBUTE))) + .isEqualTo("google.showcase.v1beta1.Echo/Echo"); + } + } + + @Test + void testMetrics_failedEcho_httpjson_recordsErrorType() throws Exception { + GoldenSignalsMetricsTracerFactory tracerFactory = + new GoldenSignalsMetricsTracerFactory(openTelemetrySdk); + + HttpTransport mockTransport = + new HttpTransport() { + @Override + protected com.google.api.client.http.LowLevelHttpRequest buildRequest( + String method, String url) { + return new com.google.api.client.http.LowLevelHttpRequest() { + @Override + public void addHeader(String name, String value) {} + + @Override + public com.google.api.client.http.LowLevelHttpResponse execute() { + return new com.google.api.client.http.LowLevelHttpResponse() { + @Override + public InputStream getContent() { + return new ByteArrayInputStream("{}".getBytes()); + } + + @Override + public String getContentEncoding() { + return null; + } + + @Override + public long getContentLength() { + return 2; + } + + @Override + public String getContentType() { + return "application/json"; + } + + @Override + public String getStatusLine() { + return "HTTP/1.1 503 Service Unavailable"; + } + + @Override + public int getStatusCode() { + return 503; + } + + @Override + public String getReasonPhrase() { + return "Service Unavailable"; + } + + @Override + public int getHeaderCount() { + return 0; + } + + @Override + public String getHeaderName(int index) { + return null; + } + + @Override + public String getHeaderValue(int index) { + return null; + } + }; + } + }; + } + }; + + EchoSettings httpJsonEchoSettings = + EchoSettings.newHttpJsonBuilder() + .setCredentialsProvider(NoCredentialsProvider.create()) + .setTransportChannelProvider( + EchoSettings.defaultHttpJsonTransportProviderBuilder() + .setHttpTransport(mockTransport) + .setEndpoint(TestClientInitializer.DEFAULT_HTTPJSON_ENDPOINT) + .build()) + .build(); + + EchoStubSettings echoStubSettings = + (EchoStubSettings) + httpJsonEchoSettings.getStubSettings().toBuilder() + .setTracerFactory(tracerFactory) + .build(); + + try (EchoClient client = EchoClient.create(echoStubSettings.createStub())) { + assertThrows( + UnavailableException.class, + () -> client.echo(EchoRequest.newBuilder().setContent("metrics-test").build())); + + Thread.sleep(100); + Collection metrics = metricReader.collectAllMetrics(); + assertThat(metrics).isNotEmpty(); + + MetricData durationMetric = + metrics.stream() + .filter(m -> m.getName().equals("gcp.client.request.duration")) + .findFirst() + .orElseThrow(() -> new AssertionError("Duration metric not found")); + + io.opentelemetry.api.common.Attributes attributes = + durationMetric.getHistogramData().getPoints().iterator().next().getAttributes(); + + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.RPC_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo("UNAVAILABLE"); + assertThat( + attributes.get(AttributeKey.longKey(ObservabilityAttributes.HTTP_RESPONSE_STATUS_ATTRIBUTE))) + .isEqualTo(503L); + assertThat( + attributes.get(AttributeKey.stringKey(ObservabilityAttributes.ERROR_TYPE_ATTRIBUTE))) + .isEqualTo("503"); + } + } +} diff --git a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java index 39e2c60f011e..5aeb4d40655c 100644 --- a/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java +++ b/sdk-platform-java/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/util/TestClientInitializer.java @@ -38,6 +38,8 @@ import com.google.showcase.v1beta1.stub.EchoStubSettings; import io.grpc.ClientInterceptor; import io.grpc.ManagedChannelBuilder; + +import java.io.IOException; import java.util.List; import java.util.Set; @@ -307,14 +309,7 @@ public static EchoClient createGrpcEchoClientOpentelemetry( .setEndpoint(DEFAULT_GRPC_ENDPOINT) .build(); - EchoStubSettings echoStubSettings = - (EchoStubSettings) - grpcEchoSettings.getStubSettings().toBuilder() - .setTracerFactory(metricsTracerFactory) - .build(); - EchoStub stub = echoStubSettings.createStub(); - - return EchoClient.create(stub); + return EchoClient.create(createStubWithServiceName(grpcEchoSettings, metricsTracerFactory)); } public static EchoClient createHttpJsonEchoClientOpentelemetry( @@ -331,14 +326,7 @@ public static EchoClient createHttpJsonEchoClientOpentelemetry( .build()) .build(); - EchoStubSettings echoStubSettings = - (EchoStubSettings) - httpJsonEchoSettings.getStubSettings().toBuilder() - .setTracerFactory(metricsTracerFactory) - .build(); - EchoStub stub = echoStubSettings.createStub(); - - return EchoClient.create(stub); + return EchoClient.create(createStubWithServiceName(httpJsonEchoSettings, metricsTracerFactory)); } public static IdentityClient createGrpcIdentityClientOpentelemetry(ApiTracerFactory tracerFactory) @@ -381,4 +369,24 @@ public static IdentityClient createHttpJsonIdentityClientOpentelemetry( .build(); return IdentityClient.create(identityStubSettings.createStub()); } + + private static EchoStub createStubWithServiceName( + EchoSettings settings, ApiTracerFactory tracingFactory) throws IOException { + EchoStubSettings.Builder builder = + (EchoStubSettings.Builder) settings.getStubSettings().toBuilder(); + builder.setTracerFactory(tracingFactory); + return new ExtendedEchoStubSettings(builder).createStub(); + } + + /** Custom wrapper to set a service name for showcase clients, which lack one by default. */ + private static class ExtendedEchoStubSettings extends EchoStubSettings { + protected ExtendedEchoStubSettings(EchoStubSettings.Builder builder) throws IOException { + super(builder); + } + + @Override + public String getServiceName() { + return "showcase"; + } + } }