From f7ec3c00f4170acba20078de604f315ca59cd6d7 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:44:39 -0500 Subject: [PATCH] add integration tests --- .../ServerSideTransactionRunner.java | 3 +- .../telemetry/BuiltinMetricsProvider.java | 4 +- .../firestore/telemetry/ClientIdentifier.java | 3 +- .../telemetry/EnabledMetricsUtil.java | 6 +- .../telemetry/TelemetryConstants.java | 1 + .../cloud/firestore/it/ITMetricsTest.java | 334 ++++++++++++++++++ .../telemetry/EnabledMetricsUtilTest.java | 22 +- 7 files changed, 362 insertions(+), 11 deletions(-) create mode 100644 google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITMetricsTest.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java index 29c92331a..f26c3e0c9 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/ServerSideTransactionRunner.java @@ -93,7 +93,7 @@ final class ServerSideTransactionRunner { firestore .getOptions() .getMetricsUtil() - .createMetricsContext(TelemetryConstants.METHOD_NAME_TRANSACTION_RUN); + .createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_TRANSACTION); } @Nonnull @@ -150,6 +150,7 @@ ApiFuture begin() { serverSideTransaction.setTransactionTraceContext(runTransactionContext); return serverSideTransaction; }); + metricsContext.recordLatencyAtFuture(MetricType.FIRST_RESPONSE_LATENCY, result); span.endAtFuture(result); return result; } catch (Exception error) { diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProvider.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProvider.java index d981e7693..56b156bdf 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProvider.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProvider.java @@ -61,7 +61,6 @@ class BuiltinMetricsProvider { private static final String MILLISECOND_UNIT = "ms"; private static final String INTEGER_UNIT = "1"; - private static final String FIRESTORE_LIBRARY_NAME = "com.google.cloud.firestore"; public BuiltinMetricsProvider(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -81,7 +80,8 @@ OpenTelemetry getOpenTelemetry() { private Map createStaticAttributes() { Map staticAttributes = new HashMap<>(); staticAttributes.put(METRIC_ATTRIBUTE_KEY_CLIENT_UID.getKey(), ClientIdentifier.getClientUid()); - staticAttributes.put(METRIC_ATTRIBUTE_KEY_LIBRARY_NAME.getKey(), FIRESTORE_LIBRARY_NAME); + staticAttributes.put( + METRIC_ATTRIBUTE_KEY_LIBRARY_NAME.getKey(), TelemetryConstants.FIRESTORE_LIBRARY_NAME); String pkgVersion = this.getClass().getPackage().getImplementationVersion(); if (pkgVersion != null) { staticAttributes.put(METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION.getKey(), pkgVersion); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/ClientIdentifier.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/ClientIdentifier.java index dc4a3eae0..71135547a 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/ClientIdentifier.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/ClientIdentifier.java @@ -22,7 +22,8 @@ import java.util.UUID; /** A utility class for retrieving a unique client identifier (CLIENT_UID) */ -final class ClientIdentifier { +@InternalApi +public final class ClientIdentifier { private ClientIdentifier() {} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtil.java index d83ebff62..106b2db35 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtil.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtil.java @@ -252,10 +252,8 @@ public void onSuccess(T result) { private void recordCounter(MetricType metric, String status) { Map attributes = createAttributes(status, methodName); - defaultMetricsProvider.counterRecorder( - MetricType.TRANSACTION_ATTEMPT_COUNT, (long) counter, attributes); - customMetricsProvider.counterRecorder( - MetricType.TRANSACTION_ATTEMPT_COUNT, (long) counter, attributes); + defaultMetricsProvider.counterRecorder(metric, (long) counter, attributes); + customMetricsProvider.counterRecorder(metric, (long) counter, attributes); } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java index d2ab8bf17..d3527dc27 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TelemetryConstants.java @@ -66,6 +66,7 @@ public interface TelemetryConstants { String METRIC_PREFIX = "custom.googleapis.com/internal/client"; String FIRESTORE_METER_NAME = "java_firestore"; String GAX_METER_NAME = OpenTelemetryMetricsRecorder.GAX_METER_NAME; + String FIRESTORE_LIBRARY_NAME = "com.google.cloud.firestore"; // Monitored resource keys for labels String RESOURCE_KEY_RESOURCE_CONTAINER = "resource_container"; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITMetricsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITMetricsTest.java new file mode 100644 index 000000000..3f7de78e8 --- /dev/null +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITMetricsTest.java @@ -0,0 +1,334 @@ +/* + * Copyright 2024 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 + * + * http://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.cloud.firestore.it; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertTrue; + +import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.Firestore; +import com.google.cloud.firestore.FirestoreOpenTelemetryOptions; +import com.google.cloud.firestore.FirestoreOptions; +import com.google.cloud.firestore.Query; +import com.google.cloud.firestore.telemetry.ClientIdentifier; +import com.google.cloud.firestore.telemetry.TelemetryConstants; +import com.google.common.base.Preconditions; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.*; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.*; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.junit.*; +import org.junit.rules.TestName; + +public class ITMetricsTest { + + private static final Logger logger = + Logger.getLogger(com.google.cloud.firestore.it.ITMetricsTest.class.getName()); + + private static OpenTelemetrySdk openTelemetrySdk; + + protected InMemoryMetricReader metricReader; + + protected Firestore firestore; + + private static Attributes expectedBaseAttributes; + + private final String ClientUid = ClientIdentifier.getClientUid(); + private final String libraryVersion = this.getClass().getPackage().getImplementationVersion(); + + @Rule public TestName testName = new TestName(); + + @Before + public void setup() { + metricReader = InMemoryMetricReader.create(); + SdkMeterProviderBuilder meterProvider = + SdkMeterProvider.builder().registerMetricReader(metricReader); + + openTelemetrySdk = OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build(); + + firestore = + FirestoreOptions.newBuilder() + .setOpenTelemetryOptions( + FirestoreOpenTelemetryOptions.newBuilder() + .setOpenTelemetry(openTelemetrySdk) + .build()) + .build() + .getService(); + + AttributesBuilder expectedAttributes = Attributes.builder(); + expectedAttributes.put( + TelemetryConstants.METRIC_ATTRIBUTE_KEY_LIBRARY_NAME.getKey(), + TelemetryConstants.FIRESTORE_LIBRARY_NAME); + expectedAttributes.put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_CLIENT_UID.getKey(), ClientUid); + if (libraryVersion != null) { + expectedAttributes.put( + TelemetryConstants.METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION.getKey(), libraryVersion); + } + + expectedBaseAttributes = expectedAttributes.build(); + } + + @After + public void tearDown() throws Exception { + Preconditions.checkNotNull( + firestore, + "Error instantiating Firestore. Check that the service account credentials were properly set."); + try { + metricReader.close(); + } finally { + // This block will always execute, even if an exception occurs above + firestore.shutdown(); + } + } + + @Test + public void simpleOperation() throws Exception { + firestore.collection("col").document().create(Collections.singletonMap("foo", "bar")).get(); + + Attributes attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, "Firestore.Commit") + .build(); + + // Validate GAX layer metrics + MetricData dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_COUNT); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_LATENCY); + validateMetricData(dataFromReader, attributes); + + // Validate SDK layer metric + attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, "DocumentReference.Create") + .build(); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_END_TO_END_LATENCY); + validateMetricData(dataFromReader, attributes); + } + + @Test + public void operationWithFirstResponseLatency() throws Exception { + firestore.collection("col").get(); + + Attributes attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, "Firestore.RunQuery") + .build(); + + // Validate GAX layer metrics + MetricData dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_COUNT); + validateMetricData(dataFromReader, attributes); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_LATENCY); + validateMetricData(dataFromReader, attributes); + + // Validate SDK layer metric + attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, "RunQuery.Get") + .build(); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_END_TO_END_LATENCY); + validateMetricData(dataFromReader, attributes); + + dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY); + validateMetricData(dataFromReader, attributes); + } + + @Test + public void operationWithTransaction() throws Exception { + firestore + .runTransaction( + transaction -> { + Query q = firestore.collection("col").whereGreaterThan("bla", ""); + DocumentReference d = firestore.collection("col").document("foo"); + DocumentReference[] docList = {d, d}; + // Document Query. + transaction.get(q).get(); + + // Aggregation Query. + transaction.get(q.count()); + + // Commit 2 documents. + transaction.set( + firestore.collection("foo").document("bar"), + Collections.singletonMap("foo", "bar")); + transaction.set( + firestore.collection("foo").document("bar2"), + Collections.singletonMap("foo2", "bar2")); + return 0; + }) + .get(); + + List attributesList = new ArrayList<>(); + String[] methods = { + "Firestore.BeginTransaction", + "Firestore.RunQuery", + "Firestore.RunAggregationQuery", + "Firestore.Commit" + }; + + for (String method : methods) { + Attributes attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, method) + .build(); + attributesList.add(attributes); + } + + // Validate GAX layer metrics + MetricData dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY); + validateMetricData(dataFromReader, attributesList); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT); + validateMetricData(dataFromReader, attributesList); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_COUNT); + validateMetricData(dataFromReader, attributesList); + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_OPERATION_LATENCY); + validateMetricData(dataFromReader, attributesList); + + // Validate SDK layer metric + List attributesList2 = new ArrayList<>(); + + String[] methods2 = { + "RunTransaction", "RunQuery.Transactional", "RunAggregationQuery.Transactional", + }; + + for (String method : methods2) { + Attributes attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, method) + .build(); + attributesList2.add(attributes); + } + dataFromReader = getMetricData(metricReader, TelemetryConstants.METRIC_NAME_END_TO_END_LATENCY); + validateMetricData(dataFromReader, attributesList2); + + dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY); + validateMetricData(dataFromReader, attributesList2); + + Attributes attributes = + expectedBaseAttributes + .toBuilder() + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, "OK") + .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, "RunTransaction") + .build(); + + dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY); + validateMetricData(dataFromReader, attributes); + + dataFromReader = + getMetricData(metricReader, TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT); + validateMetricData(dataFromReader, attributes); + } + + private void validateMetricData(MetricData metricData, Attributes expectedAttributes) { + validateMetricData(metricData, Arrays.asList(expectedAttributes)); + } + + private void validateMetricData(MetricData metricData, List expectedAttributesList) { + boolean isHistogram = metricData.getType() == MetricDataType.HISTOGRAM; + Collection points = + isHistogram + ? metricData.getHistogramData().getPoints() + : metricData.getLongSumData().getPoints(); + + assertThat(points.size()).isEqualTo(expectedAttributesList.size()); + + List actualAttributesList = new ArrayList<>(); + for (PointData point : points) { + if (isHistogram) { + assertThat(((HistogramPointData) point).getCount()).isAtLeast(1); + assertThat(((HistogramPointData) point).getSum()).isGreaterThan(0.0); + } else { + assertThat(((LongPointData) point).getValue()).isEqualTo(1); + } + actualAttributesList.add(point.getAttributes()); + } + + for (Attributes expectedAttributes : expectedAttributesList) { + assertThat( + actualAttributesList.stream() + .anyMatch( + actualAttributes -> + actualAttributes + .asMap() + .entrySet() + .containsAll(expectedAttributes.asMap().entrySet()))) + .isTrue(); + } + } + + private MetricData getMetricData(InMemoryMetricReader reader, String metricName) { + String fullMetricName = TelemetryConstants.METRIC_PREFIX + "/" + metricName; + // Fetch the MetricData with retries + for (int attemptsLeft = 1000; attemptsLeft > 0; attemptsLeft--) { + List matchingMetadata = + reader.collectAllMetrics().stream() + .filter(md -> md.getName().equals(fullMetricName)) + .collect(Collectors.toList()); + assertWithMessage( + "Found unexpected MetricData with the same name: %s, in: %s", + fullMetricName, matchingMetadata) + .that(matchingMetadata.size()) + .isAtMost(1); + + if (!matchingMetadata.isEmpty()) { + return matchingMetadata.get(0); + } + + try { + Thread.sleep(1); + } catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RuntimeException(interruptedException); + } + } + + assertTrue(String.format("MetricData is missing for metric %s", fullMetricName), false); + return null; + } +} diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtilTest.java index ee1d2992f..876e76ea0 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtilTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtilTest.java @@ -36,6 +36,8 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -98,7 +100,11 @@ public void canDisableBuiltinMetricsProviderWithFirestoreOpenTelemetryOptions() @Test public void usesCustomOpenTelemetryFromOptions() { - OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build(); + InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build(); FirestoreOptions firestoreOptions = getBaseOptions() .setOpenTelemetryOptions( @@ -114,18 +120,28 @@ public void usesCustomOpenTelemetryFromOptions() { @Test public void usesGlobalOpenTelemetryIfCustomOpenTelemetryInstanceNotProvided() { - OpenTelemetrySdk.builder().buildAndRegisterGlobal(); + InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).buildAndRegisterGlobal(); EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil(); BuiltinMetricsProvider customMetricsProvider = metricsUtil.getCustomMetricsProvider(); assertThat(customMetricsProvider).isNotNull(); assertThat(customMetricsProvider.getOpenTelemetry()).isNotNull(); assertThat(customMetricsProvider.getOpenTelemetry()).isEqualTo(GlobalOpenTelemetry.get()); + assertThat(customMetricsProvider.getOpenTelemetry().getMeterProvider()) + .isEqualTo(openTelemetry.getMeterProvider()); } @Test public void usesIndependentOpenTelemetryInstanceForDefaultAndCustomMetricsProvider() { - OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build(); + InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create(); + SdkMeterProvider sdkMeterProvider = + SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build(); + OpenTelemetry openTelemetry = + OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build(); FirestoreOptions firestoreOptions = getBaseOptions() .setOpenTelemetryOptions(