diff --git a/README.md b/README.md
index 707edc829..4a3a690e6 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ If you are using Maven with [BOM][libraries-bom], add this to your pom.xml file:
com.google.cloud
libraries-bom
- 26.61.0
+ 26.62.0
pom
import
@@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
com.google.cloud
google-cloud-firestore
- 3.31.5
+ 3.31.6
```
diff --git a/google-cloud-firestore/clirr-ignored-differences.xml b/google-cloud-firestore/clirr-ignored-differences.xml
index a1064b15c..71bbecd39 100644
--- a/google-cloud-firestore/clirr-ignored-differences.xml
+++ b/google-cloud-firestore/clirr-ignored-differences.xml
@@ -331,7 +331,7 @@
7009
void internalStream(*)
-
+
6011
@@ -346,4 +346,38 @@
void internalStream(*)
*
+
+
+
+ 6011
+ com/google/cloud/firestore/telemetry/TelemetryConstants
+ *
+
+
+
+ 6001
+ com/google/cloud/firestore/telemetry/TelemetryConstants
+ *
+
+
+
+ 6004
+ com/google/cloud/firestore/telemetry/TelemetryConstants
+ METRIC_ATTRIBUTE_KEY_*
+ io.opentelemetry.api.common.AttributeKey
+ java.lang.String
+
+
+
+ 6001
+ com/google/cloud/firestore/telemetry/TelemetryConstants$MetricType
+ END_TO_END_LATENCY
+
+
+
+ 7012
+ com/google/cloud/firestore/telemetry/MetricsUtil
+ void shutdown()
+
+
diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml
index 72f6acbc3..4b6e12366 100644
--- a/google-cloud-firestore/pom.xml
+++ b/google-cloud-firestore/pom.xml
@@ -18,6 +18,18 @@
google-cloud-firestore
1.51.0
+
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ 1.49.0 pom
+ import
+
+
+
+
${project.groupId}
@@ -141,6 +153,14 @@
+
+ io.opentelemetry
+ opentelemetry-sdk-common
+
+
+ com.google.cloud.opentelemetry
+ detector-resources-support
+
@@ -160,6 +180,12 @@
4.11.0
test
+
+ com.github.stefanbirkner
+ system-lambda
+ test
+ 1.2.1
+
com.google.api
@@ -215,12 +241,6 @@
${opentelemetry.version}
test
-
- io.opentelemetry
- opentelemetry-sdk-common
- ${opentelemetry.version}
- test
-
com.google.cloud.opentelemetry
exporter-trace
@@ -241,6 +261,18 @@
2.64.0
test
+
+ com.google.api.grpc
+ proto-google-cloud-monitoring-v3
+ 3.55.0
+ test
+
+
+ com.google.cloud
+ google-cloud-monitoring
+ 3.55.0
+ test
+
com.google.http-client
google-http-client
@@ -321,6 +353,18 @@
javax.annotation-api
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.1.2
+
+ --add-opens java.base/java.util=ALL-UNNAMED
+
+
+
+
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java
index 89702e423..21a8c9d3d 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/AggregateQuery.java
@@ -16,7 +16,6 @@
package com.google.cloud.firestore;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY;
import static com.google.cloud.firestore.telemetry.TraceUtil.ATTRIBUTE_KEY_ATTEMPT;
import com.google.api.core.ApiFuture;
@@ -29,7 +28,6 @@
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.v1.FirestoreSettings;
@@ -108,7 +106,7 @@ public ApiFuture> explain(ExplainOptions
getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_GET);
MetricsContext metricsContext =
- createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY_EXPLAIN);
+ createMetricsContext(TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_EXPLAIN);
try (Scope ignored = span.makeCurrent()) {
AggregateQueryExplainResponseDeliverer responseDeliverer =
@@ -141,8 +139,8 @@ ApiFuture get(
MetricsContext metricsContext =
createMetricsContext(
transactionId == null
- ? TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY_GET
- : TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY_TRANSACTIONAL);
+ ? TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_GET
+ : TelemetryConstants.METHOD_NAME_TRANSACTION_GET_AGGREGATION_QUERY);
try (Scope ignored = span.makeCurrent()) {
AggregateQueryResponseDeliverer responseDeliverer =
@@ -224,12 +222,11 @@ ApiFuture getFuture() {
}
void deliverFirstResponse() {
- metricsContext.recordLatency(MetricType.FIRST_RESPONSE_LATENCY);
+ metricsContext.recordLatency(TelemetryConstants.MetricType.FIRST_RESPONSE_LATENCY);
}
void deliverError(Throwable throwable) {
future.setException(throwable);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, throwable);
}
void deliverResult(
@@ -239,7 +236,6 @@ void deliverResult(
try {
T result = processResult(serverData, readTime, metrics);
future.set(result);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY);
} catch (Exception error) {
deliverError(error);
}
@@ -341,7 +337,9 @@ private boolean isExplainQuery() {
public void onStart(StreamController streamController) {
getTraceUtil()
.currentSpan()
- .addEvent(METHOD_NAME_RUN_AGGREGATION_QUERY + " Stream started.", getAttemptAttributes());
+ .addEvent(
+ TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY + " Stream started.",
+ getAttemptAttributes());
}
@Override
@@ -354,7 +352,8 @@ public void onResponse(RunAggregationQueryResponse response) {
getTraceUtil()
.currentSpan()
.addEvent(
- METHOD_NAME_RUN_AGGREGATION_QUERY + " Response Received.", getAttemptAttributes());
+ TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY + " Response Received.",
+ getAttemptAttributes());
if (response.hasReadTime()) {
readTime = Timestamp.fromProto(response.getReadTime());
}
@@ -383,7 +382,7 @@ public void onError(Throwable throwable) {
getTraceUtil()
.currentSpan()
.addEvent(
- METHOD_NAME_RUN_AGGREGATION_QUERY + ": Retryable Error",
+ TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY + ": Retryable Error",
Collections.singletonMap("error.message", throwable.toString()));
runQuery(responseDeliverer, attempt + 1);
@@ -391,7 +390,7 @@ public void onError(Throwable throwable) {
getTraceUtil()
.currentSpan()
.addEvent(
- METHOD_NAME_RUN_AGGREGATION_QUERY + ": Error",
+ TelemetryConstants.METHOD_NAME_RUN_AGGREGATION_QUERY + ": Error",
Collections.singletonMap("error.message", throwable.toString()));
responseDeliverer.deliverError(throwable);
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BulkWriter.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BulkWriter.java
index 1fc35bd35..8437f16b4 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BulkWriter.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BulkWriter.java
@@ -26,9 +26,7 @@
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.StatusCode.Code;
-import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Context;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
@@ -919,12 +917,6 @@ private void sendBatchLocked(final BulkCommitBatch batch) {
.startSpan(TelemetryConstants.METHOD_NAME_BULK_WRITER_COMMIT, traceContext)
.setAttribute(ATTRIBUTE_KEY_DOC_COUNT, batch.getMutationsSize());
- MetricsContext metricsContext =
- firestore
- .getOptions()
- .getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_BULK_WRITER_COMMIT);
-
try (Scope ignored = span.makeCurrent()) {
ApiFuture result = batch.bulkCommit();
if (!lastFlushOperation.isDone()) {
@@ -939,10 +931,8 @@ private void sendBatchLocked(final BulkCommitBatch batch) {
bulkWriterExecutor);
}
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
} else {
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionGroup.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionGroup.java
index 159f7077d..78ec902e1 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionGroup.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionGroup.java
@@ -21,9 +21,7 @@
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.ApiExceptions;
import com.google.api.gax.rpc.ApiStreamObserver;
-import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.v1.FirestoreClient.PartitionQueryPagedResponse;
@@ -114,13 +112,6 @@ public ApiFuture> getPartitions(long desiredPartitionCount)
.getTraceUtil()
.startSpan(TelemetryConstants.METHOD_NAME_PARTITION_QUERY);
- MetricsContext metricsContext =
- rpcContext
- .getFirestore()
- .getOptions()
- .getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_PARTITION_QUERY);
-
try (Scope ignored = span.makeCurrent()) {
ApiFuture> result =
ApiFutures.transform(
@@ -138,15 +129,12 @@ public ApiFuture> getPartitions(long desiredPartitionCount)
},
MoreExecutors.directExecutor());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (ApiException exception) {
span.end(exception);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, exception);
throw FirestoreException.forApiException(exception);
} catch (Throwable throwable) {
span.end(throwable);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, throwable);
throw throwable;
}
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java
index 75954d82d..b4419d9e1 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java
@@ -24,9 +24,7 @@
import com.google.api.gax.rpc.UnaryCallable;
import com.google.cloud.firestore.encoding.CustomClassMapper;
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
-import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.v1.FirestoreClient.ListDocumentsPagedResponse;
@@ -139,13 +137,6 @@ public Iterable listDocuments() {
.getTraceUtil()
.startSpan(TelemetryConstants.METHOD_NAME_COL_REF_LIST_DOCUMENTS);
- MetricsContext metricsContext =
- rpcContext
- .getFirestore()
- .getOptions()
- .getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_COL_REF_LIST_DOCUMENTS);
-
try (Scope ignored = span.makeCurrent()) {
ListDocumentsRequest.Builder request = ListDocumentsRequest.newBuilder();
request.setParent(options.getParentPath().toString());
@@ -185,15 +176,12 @@ public void remove() {
}
};
span.end();
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY);
return result;
} catch (ApiException exception) {
span.end(exception);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, exception);
throw FirestoreException.forApiException(exception);
} catch (Throwable throwable) {
span.end(throwable);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, throwable);
throw throwable;
}
}
@@ -216,13 +204,6 @@ public ApiFuture add(@Nonnull final Map field
.getTraceUtil()
.startSpan(TelemetryConstants.METHOD_NAME_COL_REF_ADD);
- MetricsContext metricsContext =
- rpcContext
- .getFirestore()
- .getOptions()
- .getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_COL_REF_ADD);
-
try (Scope ignored = span.makeCurrent()) {
final DocumentReference documentReference = document();
ApiFuture createFuture = documentReference.create(fields);
@@ -230,11 +211,9 @@ public ApiFuture add(@Nonnull final Map field
ApiFutures.transform(
createFuture, writeResult -> documentReference, MoreExecutors.directExecutor());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java
index 2b0cc1ddc..b394278dd 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java
@@ -22,9 +22,7 @@
import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.ApiExceptions;
import com.google.cloud.firestore.telemetry.MetricsUtil;
-import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.v1.FirestoreClient.ListCollectionIdsPagedResponse;
@@ -159,18 +157,13 @@ private MetricsUtil getMetricsUtil() {
public ApiFuture create(@Nonnull Map fields) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE);
-
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.create(this, fields).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -185,18 +178,14 @@ public ApiFuture create(@Nonnull Map fields) {
@Nonnull
public ApiFuture create(@Nonnull Object pojo) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_CREATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.create(this, pojo).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -211,18 +200,14 @@ public ApiFuture create(@Nonnull Object pojo) {
@Nonnull
public ApiFuture set(@Nonnull Map fields) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.set(this, fields).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -240,18 +225,14 @@ public ApiFuture set(@Nonnull Map fields) {
public ApiFuture set(
@Nonnull Map fields, @Nonnull SetOptions options) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.set(this, fields, options).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -266,18 +247,14 @@ public ApiFuture set(
@Nonnull
public ApiFuture set(@Nonnull Object pojo) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.set(this, pojo).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -294,18 +271,14 @@ public ApiFuture set(@Nonnull Object pojo) {
@Nonnull
public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions options) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_SET);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.set(this, pojo, options).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -320,18 +293,14 @@ public ApiFuture set(@Nonnull Object pojo, @Nonnull SetOptions opti
@Nonnull
public ApiFuture update(@Nonnull Map fields) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.update(this, fields).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -347,19 +316,15 @@ public ApiFuture update(@Nonnull Map fields) {
@Nonnull
public ApiFuture update(@Nonnull Map fields, Precondition options) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result =
extractFirst(writeBatch.update(this, fields, options).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -377,19 +342,15 @@ public ApiFuture update(@Nonnull Map fields, Precon
public ApiFuture update(
@Nonnull String field, @Nullable Object value, Object... moreFieldsAndValues) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result =
extractFirst(writeBatch.update(this, field, value, moreFieldsAndValues).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -407,19 +368,15 @@ public ApiFuture update(
public ApiFuture update(
@Nonnull FieldPath fieldPath, @Nullable Object value, Object... moreFieldsAndValues) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result =
extractFirst(writeBatch.update(this, fieldPath, value, moreFieldsAndValues).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -441,8 +398,6 @@ public ApiFuture update(
@Nullable Object value,
Object... moreFieldsAndValues) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
@@ -450,11 +405,9 @@ public ApiFuture update(
extractFirst(
writeBatch.update(this, options, field, value, moreFieldsAndValues).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -476,8 +429,6 @@ public ApiFuture update(
@Nullable Object value,
Object... moreFieldsAndValues) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_UPDATE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
@@ -485,11 +436,9 @@ public ApiFuture update(
extractFirst(
writeBatch.update(this, options, fieldPath, value, moreFieldsAndValues).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -503,18 +452,14 @@ public ApiFuture update(
@Nonnull
public ApiFuture delete(@Nonnull Precondition options) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.delete(this, options).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -527,18 +472,14 @@ public ApiFuture delete(@Nonnull Precondition options) {
@Nonnull
public ApiFuture delete() {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_DELETE);
try (Scope ignored = span.makeCurrent()) {
WriteBatch writeBatch = rpcContext.getFirestore().batch();
ApiFuture result = extractFirst(writeBatch.delete(this).commit());
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -553,17 +494,13 @@ public ApiFuture delete() {
@Nonnull
public ApiFuture get() {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_GET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_GET);
try (Scope ignored = span.makeCurrent()) {
ApiFuture result = extractFirst(rpcContext.getFirestore().getAll(this));
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -579,18 +516,14 @@ public ApiFuture get() {
@Nonnull
public ApiFuture get(FieldMask fieldMask) {
TraceUtil.Span span = getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_GET);
- MetricsContext metricsContext =
- getMetricsUtil().createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_GET);
try (Scope ignored = span.makeCurrent()) {
ApiFuture result =
extractFirst(rpcContext.getFirestore().getAll(new DocumentReference[] {this}, fieldMask));
span.endAtFuture(result);
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -605,9 +538,6 @@ public ApiFuture get(FieldMask fieldMask) {
public Iterable listCollections() {
TraceUtil.Span span =
getTraceUtil().startSpan(TelemetryConstants.METHOD_NAME_DOC_REF_LIST_COLLECTIONS);
- MetricsContext metricsContext =
- getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_DOC_REF_LIST_COLLECTIONS);
try (Scope ignored = span.makeCurrent()) {
ListCollectionIdsRequest.Builder request = ListCollectionIdsRequest.newBuilder();
@@ -642,11 +572,9 @@ public void remove() {
}
};
span.end();
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY);
return result;
} catch (ApiException exception) {
span.end(exception);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, exception);
throw FirestoreException.forApiException(exception);
}
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java
index 4d532b459..7e7408aa4 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreImpl.java
@@ -36,7 +36,6 @@
import com.google.cloud.firestore.spi.v1.FirestoreRpc;
import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
@@ -236,8 +235,8 @@ void getAll(
.getMetricsUtil()
.createMetricsContext(
transactionId == null
- ? TelemetryConstants.METHOD_NAME_BATCH_GET_DOCUMENTS_GET_ALL
- : TelemetryConstants.METHOD_NAME_BATCH_GET_DOCUMENTS_TRANSACTIONAL);
+ ? TelemetryConstants.METHOD_NAME_BATCH_GET_DOCUMENTS
+ : TelemetryConstants.METHOD_NAME_TRANSACTION_BATCH_GET_DOCUMENTS);
ResponseObserver responseObserver =
new ResponseObserver() {
@@ -268,7 +267,7 @@ public void onResponse(BatchGetDocumentsResponse response) {
.addEvent(
TelemetryConstants.METHOD_NAME_BATCH_GET_DOCUMENTS
+ ": First response received");
- metricsContext.recordLatency(MetricType.FIRST_RESPONSE_LATENCY);
+ metricsContext.recordLatency(TelemetryConstants.MetricType.FIRST_RESPONSE_LATENCY);
} else if (numResponses % NUM_RESPONSES_PER_TRACE_EVENT == 0) {
getTraceUtil()
.currentSpan()
@@ -313,7 +312,6 @@ public void onResponse(BatchGetDocumentsResponse response) {
@Override
public void onError(Throwable throwable) {
getTraceUtil().currentSpan().end(throwable);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, throwable);
apiStreamObserver.onError(throwable);
}
@@ -329,7 +327,6 @@ public void onComplete() {
+ numResponses
+ " responses.",
Collections.singletonMap(ATTRIBUTE_KEY_NUM_RESPONSES, numResponses));
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY);
apiStreamObserver.onCompleted();
}
};
@@ -444,10 +441,6 @@ public ApiFuture runAsyncTransaction(
@Nonnull final Transaction.AsyncFunction updateFunction,
@Nonnull TransactionOptions transactionOptions) {
- MetricsContext metricsContext =
- getOptions()
- .getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_TRANSACTION);
ApiFuture result;
try {
@@ -462,9 +455,7 @@ public ApiFuture runAsyncTransaction(
// that cannot be tracked client side.
result = new ServerSideTransactionRunner<>(this, updateFunction, transactionOptions).run();
}
- metricsContext.recordLatencyAtFuture(MetricType.END_TO_END_LATENCY, result);
} catch (Exception error) {
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
return result;
@@ -558,18 +549,21 @@ public FirestoreOptions getOptions() {
@Override
public void close() throws Exception {
firestoreClient.close();
+ firestoreOptions.getMetricsUtil().shutdown();
closed = true;
}
@Override
public void shutdown() {
firestoreClient.shutdown();
+ firestoreOptions.getMetricsUtil().shutdown();
closed = true;
}
@Override
public void shutdownNow() {
firestoreClient.shutdownNow();
+ firestoreOptions.getMetricsUtil().shutdown();
closed = true;
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOpenTelemetryOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOpenTelemetryOptions.java
index 6fe0c4375..ab54fc0fd 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOpenTelemetryOptions.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOpenTelemetryOptions.java
@@ -59,8 +59,7 @@ public static class Builder {
@Nullable private OpenTelemetry openTelemetry;
private Builder() {
- // TODO(metrics): default this to true when feature is ready
- exportBuiltinMetricsToGoogleCloudMonitoring = false;
+ exportBuiltinMetricsToGoogleCloudMonitoring = true;
openTelemetry = null;
}
@@ -75,7 +74,6 @@ public FirestoreOpenTelemetryOptions build() {
return new FirestoreOpenTelemetryOptions(this);
}
- // TODO(metrics): make this public when feature is ready.
/**
* Sets whether built-in metrics should be exported to Google Cloud Monitoring
*
@@ -83,7 +81,7 @@ public FirestoreOpenTelemetryOptions build() {
* Monitoring.
*/
@Nonnull
- private FirestoreOpenTelemetryOptions.Builder exportBuiltinMetricsToGoogleCloudMonitoring(
+ public FirestoreOpenTelemetryOptions.Builder exportBuiltinMetricsToGoogleCloudMonitoring(
boolean exportBuiltinMetrics) {
this.exportBuiltinMetricsToGoogleCloudMonitoring = exportBuiltinMetrics;
return this;
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java
index 2bf9e98f9..238870f50 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java
@@ -35,8 +35,6 @@
import com.google.cloud.Timestamp;
import com.google.cloud.firestore.Query.QueryOptions.Builder;
import com.google.cloud.firestore.encoding.CustomClassMapper;
-import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
-import com.google.cloud.firestore.telemetry.TelemetryConstants;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.firestore.bundle.BundledQuery;
@@ -1486,9 +1484,6 @@ public void stream(@Nonnull final ApiStreamObserver responseOb
"Query results for queries that include limitToLast() constraints cannot be streamed. "
+ "Use Query.get() instead.");
- MetricsContext metricsContext =
- createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_QUERY_GET);
-
ApiStreamObserver observer =
new ApiStreamObserver() {
@Override
@@ -1514,7 +1509,7 @@ public void onCompleted() {
};
internalStream(
- new MonitoredStreamResponseObserver(observer, metricsContext),
+ observer,
/* startTimeNanos= */ rpcContext.getClock().nanoTime(),
/* transactionId= */ null,
/* readTime= */ null,
@@ -1538,9 +1533,6 @@ public ApiFuture explainStream(
"Query results for queries that include limitToLast() constraints cannot be streamed. "
+ "Use Query.explain() instead.");
- MetricsContext metricsContext =
- createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_QUERY_EXPLAIN);
-
final SettableApiFuture metricsFuture = SettableApiFuture.create();
ApiStreamObserver observer =
@@ -1578,7 +1570,7 @@ public void onCompleted() {
};
internalStream(
- new MonitoredStreamResponseObserver(observer, metricsContext),
+ observer,
/* startTimeNanos= */ rpcContext.getClock().nanoTime(),
/* transactionId= */ null,
/* readTime= */ null,
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 839d549a5..2baf8acdb 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
@@ -28,7 +28,6 @@
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.firestore.telemetry.MetricsUtil.MetricsContext;
import com.google.cloud.firestore.telemetry.TelemetryConstants;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Scope;
import com.google.cloud.firestore.telemetry.TraceUtil.Span;
@@ -93,7 +92,7 @@ final class ServerSideTransactionRunner {
firestore
.getOptions()
.getMetricsUtil()
- .createMetricsContext(TelemetryConstants.METHOD_NAME_TRANSACTION_RUN);
+ .createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_TRANSACTION);
}
@Nonnull
@@ -103,8 +102,9 @@ private TraceUtil getTraceUtil() {
ApiFuture run() {
ApiFuture result = runInternally();
- metricsContext.recordLatencyAtFuture(MetricType.TRANSACTION_LATENCY, result);
- metricsContext.recordCounterAtFuture(MetricType.TRANSACTION_ATTEMPT_COUNT, result);
+ metricsContext.recordLatencyAtFuture(TelemetryConstants.MetricType.TRANSACTION_LATENCY, result);
+ metricsContext.recordCounterAtFuture(
+ TelemetryConstants.MetricType.TRANSACTION_ATTEMPT_COUNT, result);
return result;
}
@@ -150,6 +150,13 @@ ApiFuture begin() {
serverSideTransaction.setTransactionTraceContext(runTransactionContext);
return serverSideTransaction;
});
+
+ // Record the first time latency for the first BeginTransaction call only
+ Boolean isFirstAttempt = (transactionOptions.getNumberOfAttempts() - attemptsRemaining) == 1;
+ if (isFirstAttempt) {
+ metricsContext.recordLatencyAtFuture(
+ TelemetryConstants.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/StreamableQuery.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java
index 6e331af22..6bce85c9f 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/StreamableQuery.java
@@ -114,12 +114,6 @@ ApiFuture get(
? TelemetryConstants.METHOD_NAME_QUERY_GET
: TelemetryConstants.METHOD_NAME_TRANSACTION_GET_QUERY);
- MetricsContext metricsContext =
- createMetricsContext(
- transactionId != null
- ? TelemetryConstants.METHOD_NAME_RUN_QUERY_TRANSACTIONAL
- : TelemetryConstants.METHOD_NAME_RUN_QUERY_GET);
-
try (Scope ignored = span.makeCurrent()) {
final SettableApiFuture result = SettableApiFuture.create();
@@ -161,7 +155,7 @@ public void onCompleted() {
};
internalStream(
- new MonitoredStreamResponseObserver(observer, metricsContext),
+ observer,
/* startTimeNanos= */ rpcContext.getClock().nanoTime(),
transactionId,
/* readTime= */ requestReadTime,
@@ -172,7 +166,6 @@ public void onCompleted() {
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
@@ -193,9 +186,6 @@ public ApiFuture> explain(ExplainOptions options) {
.getTraceUtil()
.startSpan(TelemetryConstants.METHOD_NAME_QUERY_GET);
- MetricsContext metricsContext =
- createMetricsContext(TelemetryConstants.METHOD_NAME_RUN_QUERY_EXPLAIN);
-
try (Scope ignored = span.makeCurrent()) {
final SettableApiFuture> result = SettableApiFuture.create();
@@ -255,7 +245,7 @@ public void onCompleted() {
};
internalStream(
- new MonitoredStreamResponseObserver(observer, metricsContext),
+ observer,
/* startTimeNanos= */ rpcContext.getClock().nanoTime(),
/* transactionId= */ null,
/* readTime= */ null,
@@ -266,47 +256,12 @@ public void onCompleted() {
return result;
} catch (Exception error) {
span.end(error);
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, error);
throw error;
}
}
- class MonitoredStreamResponseObserver implements ApiStreamObserver {
- private final ApiStreamObserver observer;
- private final MetricsContext metricsContext;
- private boolean receivedFirstResponse = false;
-
- // Constructor to initialize with the delegate and MetricsContext
- public MonitoredStreamResponseObserver(
- ApiStreamObserver observer, MetricsContext metricsContext) {
- this.observer = observer;
- this.metricsContext = metricsContext;
- }
-
- @Override
- public void onNext(RunQueryResponse value) {
- if (!receivedFirstResponse) {
- receivedFirstResponse = true;
- metricsContext.recordLatency(MetricType.FIRST_RESPONSE_LATENCY);
- }
- observer.onNext(value);
- }
-
- @Override
- public void onError(Throwable t) {
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY, t);
- observer.onError(t);
- }
-
- @Override
- public void onCompleted() {
- metricsContext.recordLatency(MetricType.END_TO_END_LATENCY);
- observer.onCompleted();
- }
- }
-
void internalStream(
- final MonitoredStreamResponseObserver streamResponseObserver,
+ final ApiStreamObserver runQueryResponseObserver,
final long startTimeNanos,
@Nullable final ByteString transactionId,
@Nullable final Timestamp readTime,
@@ -326,6 +281,14 @@ void internalStream(
.put(ATTRIBUTE_KEY_IS_RETRY_WITH_CURSOR, isRetryRequestWithCursor)
.build());
+ String methodName =
+ (transactionId != null)
+ ? TelemetryConstants.METHOD_NAME_TRANSACTION_GET_QUERY
+ : (explainOptions != null
+ ? TelemetryConstants.METHOD_NAME_QUERY_EXPLAIN
+ : TelemetryConstants.METHOD_NAME_QUERY_GET);
+ MetricsContext metricsContext = createMetricsContext(methodName);
+
final AtomicReference lastReceivedDocument = new AtomicReference<>();
ResponseObserver observer =
@@ -346,9 +309,10 @@ public void onResponse(RunQueryResponse response) {
if (!firstResponse) {
firstResponse = true;
currentSpan.addEvent(TelemetryConstants.METHOD_NAME_RUN_QUERY + ": First Response");
+ metricsContext.recordLatency(MetricType.FIRST_RESPONSE_LATENCY);
}
- streamResponseObserver.onNext(response);
+ runQueryResponseObserver.onNext(response);
if (response.hasDocument()) {
numDocuments++;
@@ -383,7 +347,7 @@ public void onError(Throwable throwable) {
startAfter(cursor)
.internalStream(
- streamResponseObserver,
+ runQueryResponseObserver,
startTimeNanos,
/* transactionId= */ null,
options.getRequireConsistency() ? cursor.getReadTime() : null,
@@ -393,7 +357,7 @@ public void onError(Throwable throwable) {
currentSpan.addEvent(
TelemetryConstants.METHOD_NAME_RUN_QUERY + ": Error",
Collections.singletonMap("error.message", throwable.toString()));
- streamResponseObserver.onError(throwable);
+ runQueryResponseObserver.onError(throwable);
}
}
@@ -404,7 +368,7 @@ public void onComplete() {
currentSpan.addEvent(
TelemetryConstants.METHOD_NAME_RUN_QUERY + ": Completed",
Collections.singletonMap(ATTRIBUTE_KEY_DOC_COUNT, numDocuments));
- streamResponseObserver.onCompleted();
+ runQueryResponseObserver.onCompleted();
}
boolean shouldRetry(DocumentSnapshot lastDocument, Throwable t) {
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java
index ff1b7e2d6..04d83a1a1 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Transaction.java
@@ -18,7 +18,6 @@
import com.google.api.core.ApiFuture;
import com.google.api.core.InternalExtensionOnly;
-import com.google.cloud.firestore.telemetry.MetricsUtil;
import com.google.cloud.firestore.telemetry.TraceUtil;
import com.google.cloud.firestore.telemetry.TraceUtil.Context;
import java.util.List;
@@ -50,12 +49,6 @@ TraceUtil getTraceUtil() {
return firestore.getOptions().getTraceUtil();
}
- // TODO(Metrics): implement transaction latency and attempt count metrics
- @Nonnull
- MetricsUtil getMetricsUtil() {
- return firestore.getOptions().getMetricsUtil();
- }
-
@Nonnull
Context setTransactionTraceContext(Context context) {
return transactionTraceContext = context;
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java
index cfa852ce4..60cb49422 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java
@@ -621,6 +621,7 @@ ApiFuture> commit(@Nullable ByteString transactionId) {
: TelemetryConstants.METHOD_NAME_TRANSACTION_COMMIT);
span.setAttribute(ATTRIBUTE_KEY_DOC_COUNT, writes.size());
span.setAttribute(ATTRIBUTE_KEY_IS_TRANSACTIONAL, transactionId != null);
+
try (Scope ignored = span.makeCurrent()) {
// Sequence is thread safe.
//
@@ -654,9 +655,11 @@ ApiFuture> commit(@Nullable ByteString transactionId) {
},
MoreExecutors.directExecutor());
span.endAtFuture(returnValue);
+
return returnValue;
} catch (Exception error) {
span.end(error);
+
throw 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 621efb914..abbcd4763 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
@@ -16,20 +16,12 @@
package com.google.cloud.firestore.telemetry;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.FIRESTORE_METER_NAME;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_ATTRIBUTE_KEY_CLIENT_UID;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_ATTRIBUTE_KEY_LIBRARY_NAME;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_NAME_END_TO_END_LATENCY;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_PREFIX;
+import static com.google.cloud.firestore.telemetry.TelemetryConstants.*;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.api.gax.tracing.MetricsTracerFactory;
import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder;
-import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
+import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributesBuilder;
@@ -50,7 +42,6 @@ class BuiltinMetricsProvider {
private static final Logger logger = Logger.getLogger(BuiltinMetricsProvider.class.getName());
private OpenTelemetry openTelemetry;
- private DoubleHistogram endToEndLatency;
private DoubleHistogram firstResponseLatency;
private DoubleHistogram transactionLatency;
private LongCounter transactionAttemptCount;
@@ -60,26 +51,27 @@ 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;
this.staticAttributes = createStaticAttributes();
if (openTelemetry.getMeterProvider() != MeterProvider.noop()) {
- configureRPCLayerMetrics();
configureSDKLayerMetrics();
+ configureRPCLayerMetrics();
}
}
+ @VisibleForTesting
+ OpenTelemetry getOpenTelemetry() {
+ return openTelemetry;
+ }
+
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);
- String pkgVersion = this.getClass().getPackage().getImplementationVersion();
- if (pkgVersion != null) {
- staticAttributes.put(METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION.getKey(), pkgVersion);
- }
+ staticAttributes.put(METRIC_ATTRIBUTE_KEY_CLIENT_UID, ClientIdentifier.getClientUid());
+ staticAttributes.put(METRIC_ATTRIBUTE_KEY_SERVICE, FIRESTORE_SERVICE);
+
return staticAttributes;
}
@@ -94,13 +86,6 @@ private void configureRPCLayerMetrics() {
private void configureSDKLayerMetrics() {
Meter meter = openTelemetry.getMeter(FIRESTORE_METER_NAME);
- this.endToEndLatency =
- meter
- .histogramBuilder(METRIC_PREFIX + "/" + METRIC_NAME_END_TO_END_LATENCY)
- .setDescription("Firestore operations' end-to-end latency")
- .setUnit(MILLISECOND_UNIT)
- .build();
-
this.firstResponseLatency =
meter
.histogramBuilder(METRIC_PREFIX + "/" + METRIC_NAME_FIRST_RESPONSE_LATENCY)
@@ -154,8 +139,6 @@ public void counterRecorder(MetricType metricType, long count, Map void recordCounterAtFuture(MetricType metric, ApiFuture futureValu
public void incrementCounter() {}
}
+ @Override
+ public void shutdown() {}
+
@Override
public MetricsContext createMetricsContext(String methodName) {
return new MetricsContext();
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 e8ebfc355..48513aadc 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
@@ -16,25 +16,25 @@
package com.google.cloud.firestore.telemetry;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.COMMON_ATTRIBUTES;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.FIRESTORE_METER_NAME;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.FIRESTORE_METRICS;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.GAX_METER_NAME;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.GAX_METRICS;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS;
-import static com.google.cloud.firestore.telemetry.TelemetryConstants.METRIC_PREFIX;
+import static com.google.cloud.firestore.telemetry.TelemetryConstants.*;
+import static com.google.cloud.opentelemetry.detection.GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
+import com.google.api.gax.rpc.ApiException;
import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.tracing.ApiTracerFactory;
import com.google.cloud.firestore.FirestoreException;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
+import com.google.cloud.opentelemetry.detection.AttributeKeys;
+import com.google.cloud.opentelemetry.detection.DetectedPlatform;
+import com.google.cloud.opentelemetry.detection.GCPPlatformDetector;
import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
import com.google.cloud.opentelemetry.metric.MetricConfiguration;
+import com.google.cloud.opentelemetry.metric.MonitoredResourceDescription;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
@@ -42,14 +42,17 @@
import io.grpc.Status;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
-import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.InstrumentSelector;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
import io.opentelemetry.sdk.metrics.View;
import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.sdk.metrics.export.MetricReader;
import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
+import io.opentelemetry.sdk.resources.Resource;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
@@ -65,16 +68,20 @@
* `FirestoreOpenTelemetryOptions` in `FirestoreOptions` can be used to configure its behavior.
*/
class EnabledMetricsUtil implements MetricsUtil {
+
+ private FirestoreOptions firestoreOptions;
private BuiltinMetricsProvider defaultMetricsProvider;
private BuiltinMetricsProvider customMetricsProvider;
+ private MetricReader metricReader;
private static final Logger logger = Logger.getLogger(EnabledMetricsUtil.class.getName());
EnabledMetricsUtil(FirestoreOptions firestoreOptions) {
+ this.firestoreOptions = firestoreOptions;
try {
- configureDefaultMetricsProvider(firestoreOptions);
- configureCustomMetricsProvider(firestoreOptions);
- } catch (IOException e) {
+ this.defaultMetricsProvider = configureDefaultMetricsProvider();
+ this.customMetricsProvider = configureCustomMetricsProvider();
+ } catch (Exception e) {
logger.warning(
"Unable to create MetricsUtil object for client side metrics, will skip exporting client"
+ " side metrics"
@@ -82,27 +89,33 @@ class EnabledMetricsUtil implements MetricsUtil {
}
}
- private void configureDefaultMetricsProvider(FirestoreOptions firestoreOptions)
- throws IOException {
- OpenTelemetry defaultOpenTelemetry;
- boolean exportBuiltinMetricsToGoogleCloudMonitoring =
- firestoreOptions.getOpenTelemetryOptions().exportBuiltinMetricsToGoogleCloudMonitoring();
- if (exportBuiltinMetricsToGoogleCloudMonitoring) {
- defaultOpenTelemetry = getDefaultOpenTelemetryInstance(firestoreOptions.getProjectId());
- } else {
- defaultOpenTelemetry = OpenTelemetry.noop();
+ private BuiltinMetricsProvider configureDefaultMetricsProvider() {
+ OpenTelemetry defaultOpenTelemetry = OpenTelemetry.noop();
+ if (firestoreOptions.getOpenTelemetryOptions().exportBuiltinMetricsToGoogleCloudMonitoring()) {
+ if (firestoreOptions.getProjectId() == null) {
+ logger.warning(
+ "Project ID is null, skipping client side metrics export to Cloud Monitoring.");
+ } else {
+ try {
+ defaultOpenTelemetry = getDefaultOpenTelemetryInstance();
+ } catch (Exception e) {
+ logger.warning(
+ "Unable to create default OpenTelemetry instance for client side metrics, will skip"
+ + " exporting client side metrics to Cloud Monitoring: "
+ + e);
+ }
+ }
}
- this.defaultMetricsProvider = new BuiltinMetricsProvider(defaultOpenTelemetry);
+ return new BuiltinMetricsProvider(defaultOpenTelemetry);
}
- private void configureCustomMetricsProvider(FirestoreOptions firestoreOptions)
- throws IOException {
+ private BuiltinMetricsProvider configureCustomMetricsProvider() throws IOException {
OpenTelemetry customOpenTelemetry =
firestoreOptions.getOpenTelemetryOptions().getOpenTelemetry();
if (customOpenTelemetry == null) {
customOpenTelemetry = GlobalOpenTelemetry.get();
}
- this.customMetricsProvider = new BuiltinMetricsProvider(customOpenTelemetry);
+ return new BuiltinMetricsProvider(customOpenTelemetry);
}
@Override
@@ -116,11 +129,21 @@ public void addMetricsTracerFactory(List apiTracerFactories) {
addTracerFactory(apiTracerFactories, customMetricsProvider);
}
+ @VisibleForTesting
+ BuiltinMetricsProvider getCustomMetricsProvider() {
+ return customMetricsProvider;
+ }
+
+ @VisibleForTesting
+ BuiltinMetricsProvider getDefaultMetricsProvider() {
+ return defaultMetricsProvider;
+ }
+
/**
* Creates a default {@link OpenTelemetry} instance to collect and export built-in client side
* metrics to Google Cloud Monitoring.
*/
- private OpenTelemetry getDefaultOpenTelemetryInstance(String projectId) throws IOException {
+ private OpenTelemetry getDefaultOpenTelemetryInstance() throws IOException {
SdkMeterProviderBuilder sdkMeterProviderBuilder = SdkMeterProvider.builder();
// Filter out attributes that are not defined
@@ -128,18 +151,54 @@ private OpenTelemetry getDefaultOpenTelemetryInstance(String projectId) throws I
sdkMeterProviderBuilder.registerView(entry.getKey(), entry.getValue());
}
+ sdkMeterProviderBuilder.setResource(Resource.create(createResourceAttributes()));
+
+ MonitoredResourceDescription monitoredResourceMapping =
+ new MonitoredResourceDescription(FIRESTORE_RESOURCE_TYPE, FIRESTORE_RESOURCE_LABELS);
+
+ // TODO: uncomment the configuration below
MetricExporter metricExporter =
GoogleCloudMetricExporter.createWithConfiguration(
MetricConfiguration.builder()
- .setProjectId(projectId)
+ .setProjectId(firestoreOptions.getProjectId())
+ // .setPrefix("firestore.googleapis.com")
// Ignore library info as it is collected by the metric attributes as well
.setInstrumentationLibraryLabelsEnabled(false)
+ // .setMonitoredResourceDescription(monitoredResourceMapping)
+ // .setUseServiceTimeSeries(true)
.build());
- sdkMeterProviderBuilder.registerMetricReader(PeriodicMetricReader.create(metricExporter));
+ metricReader = PeriodicMetricReader.create(metricExporter);
+ sdkMeterProviderBuilder.registerMetricReader(metricReader);
return OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProviderBuilder.build()).build();
}
+ Attributes createResourceAttributes() {
+ AttributesBuilder attributesBuilder =
+ Attributes.builder()
+ .put(RESOURCE_KEY_LOCATION, detectClientLocation())
+ .put(RESOURCE_KEY_PROJECT, firestoreOptions.getProjectId())
+ .put(RESOURCE_KEY_DATABASE, firestoreOptions.getDatabaseId())
+ .put(RESOURCE_KEY_UID, ClientIdentifier.getClientUid());
+
+ String pkgVersion = this.getClass().getPackage().getImplementationVersion();
+ attributesBuilder.put(
+ RESOURCE_KEY_INSTANCE, "java_" + (pkgVersion != null ? pkgVersion : "unknown"));
+
+ return attributesBuilder.build();
+ }
+
+ private String detectClientLocation() {
+ GCPPlatformDetector detector = GCPPlatformDetector.DEFAULT_INSTANCE;
+ DetectedPlatform detectedPlatform = detector.detectPlatform();
+ // All platform except GKE uses "cloud_region" for region attribute.
+ String region = detectedPlatform.getAttributes().get("cloud_region");
+ if (detectedPlatform.getSupportedPlatform() == GOOGLE_KUBERNETES_ENGINE) {
+ region = detectedPlatform.getAttributes().get(AttributeKeys.GKE_LOCATION_TYPE_REGION);
+ }
+ return region == null ? "global" : region;
+ }
+
private static Map getAllViews() {
ImmutableMap.Builder views = ImmutableMap.builder();
GAX_METRICS.forEach(metric -> defineView(views, metric, GAX_METER_NAME));
@@ -153,8 +212,7 @@ private static void defineView(
InstrumentSelector.builder().setMeterName(meter).setName(METRIC_PREFIX + "/" + id).build();
Set attributesFilter =
ImmutableSet.builder()
- .addAll(
- COMMON_ATTRIBUTES.stream().map(AttributeKey::getKey).collect(Collectors.toSet()))
+ .addAll(COMMON_ATTRIBUTES.stream().collect(Collectors.toSet()))
.build();
View view = View.builder().setAttributeFilter(attributesFilter).build();
@@ -169,6 +227,19 @@ private void addTracerFactory(
}
}
+ @Override
+ public void shutdown() {
+ // Gracefully shutdown the metric reader registered to the default OTEL instance inside the sdk.
+ if (metricReader != null) {
+ try {
+ metricReader.shutdown();
+ } catch (Exception e) {
+ // Handle the exception or retry with exponential backoff
+ logger.warning("Error shutting down MetricReader: " + e.getMessage());
+ }
+ }
+ }
+
class MetricsContext implements MetricsUtil.MetricsContext {
private final Stopwatch stopwatch;
private int counter;
@@ -235,28 +306,30 @@ 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);
}
}
private Map createAttributes(String status, String methodName) {
Map attributes = new HashMap<>();
- attributes.put(METRIC_ATTRIBUTE_KEY_METHOD.getKey(), methodName);
- attributes.put(METRIC_ATTRIBUTE_KEY_STATUS.getKey(), status);
+ attributes.put(METRIC_ATTRIBUTE_KEY_METHOD, methodName);
+ attributes.put(METRIC_ATTRIBUTE_KEY_STATUS, status);
return attributes;
}
- private String extractErrorStatus(@Nullable Throwable throwable) {
- if (!(throwable instanceof FirestoreException)) {
- return StatusCode.Code.UNKNOWN.toString();
+ @VisibleForTesting
+ String extractErrorStatus(@Nullable Throwable throwable) {
+ Status status = null;
+
+ if (throwable instanceof FirestoreException) {
+ status = ((FirestoreException) throwable).getStatus();
+ } else if (throwable instanceof ApiException) {
+ status = FirestoreException.forApiException((ApiException) throwable).getStatus();
}
- Status status = ((FirestoreException) throwable).getStatus();
if (status == null) {
- return StatusCode.Code.UNKNOWN.toString();
+ return Status.Code.UNKNOWN.toString();
}
return status.getCode().name();
}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/MetricsUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/MetricsUtil.java
index 02ac3a26b..67acbcf08 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/MetricsUtil.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/MetricsUtil.java
@@ -52,11 +52,12 @@ static MetricsUtil getInstance(@Nonnull FirestoreOptions firestoreOptions) {
}
}
+ abstract void shutdown();
+
static boolean shouldCreateEnabledInstance() {
// Client side metrics feature is default on unless it is manually turned off by
// environment variables
- // TODO(metrics): The feature is disabled before it is ready for general release.
- boolean shouldCreateEnabledInstance = false;
+ boolean shouldCreateEnabledInstance = true;
String enableMetricsEnvVar = System.getenv(ENABLE_METRICS_ENV_VAR);
if (enableMetricsEnvVar != null) {
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..eae7db6a5 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
@@ -19,7 +19,6 @@
import com.google.api.core.InternalApi;
import com.google.api.gax.tracing.OpenTelemetryMetricsRecorder;
import com.google.common.collect.ImmutableSet;
-import io.opentelemetry.api.common.AttributeKey;
import java.util.Set;
/** Constants used for telemetry in the Firestore SDK. */
@@ -35,24 +34,19 @@ public interface TelemetryConstants {
String METHOD_NAME_COL_REF_ADD = "CollectionReference.Add";
String METHOD_NAME_COL_REF_LIST_DOCUMENTS = "CollectionReference.ListDocuments";
String METHOD_NAME_QUERY_GET = "Query.Get";
+ String METHOD_NAME_QUERY_EXPLAIN = "Query.Explain";
String METHOD_NAME_AGGREGATION_QUERY_GET = "AggregationQuery.Get";
+ String METHOD_NAME_AGGREGATION_QUERY_EXPLAIN = "AggregationQuery.Explain";
String METHOD_NAME_RUN_QUERY = "RunQuery";
- String METHOD_NAME_RUN_QUERY_EXPLAIN = "RunQuery.Explain";
- String METHOD_NAME_RUN_QUERY_GET = "RunQuery.Get";
- String METHOD_NAME_RUN_QUERY_TRANSACTIONAL = "RunQuery.Transactional";
String METHOD_NAME_RUN_AGGREGATION_QUERY = "RunAggregationQuery";
- String METHOD_NAME_RUN_AGGREGATION_QUERY_EXPLAIN = "RunAggregationQuery.Explain";
- String METHOD_NAME_RUN_AGGREGATION_QUERY_GET = "RunAggregationQuery.Get";
- String METHOD_NAME_RUN_AGGREGATION_QUERY_TRANSACTIONAL = "RunAggregationQuery.Transactional";
String METHOD_NAME_BATCH_GET_DOCUMENTS = "BatchGetDocuments";
- String METHOD_NAME_BATCH_GET_DOCUMENTS_GET_ALL = "BatchGetDocuments.GetAll";
- String METHOD_NAME_BATCH_GET_DOCUMENTS_TRANSACTIONAL = "BatchGetDocuments.Transactional";
String METHOD_NAME_TRANSACTION_RUN = "Transaction.Run";
String METHOD_NAME_TRANSACTION_BEGIN = "Transaction.Begin";
String METHOD_NAME_TRANSACTION_GET_QUERY = "Transaction.Get.Query";
String METHOD_NAME_TRANSACTION_GET_AGGREGATION_QUERY = "Transaction.Get.AggregationQuery";
String METHOD_NAME_TRANSACTION_GET_DOCUMENT = "Transaction.Get.Document";
String METHOD_NAME_TRANSACTION_GET_DOCUMENTS = "Transaction.Get.Documents";
+ String METHOD_NAME_TRANSACTION_BATCH_GET_DOCUMENTS = "Transaction.BatchGetDocuments";
String METHOD_NAME_TRANSACTION_ROLLBACK = "Transaction.Rollback";
String METHOD_NAME_BATCH_COMMIT = "Batch.Commit";
String METHOD_NAME_TRANSACTION_COMMIT = "Transaction.Commit";
@@ -60,35 +54,50 @@ public interface TelemetryConstants {
String METHOD_NAME_BULK_WRITER_COMMIT = "BulkWriter.Commit";
String METHOD_NAME_RUN_TRANSACTION = "RunTransaction";
- // OpenTelemetry built-in metrics constants
- String FIRESTORE_RESOURCE_TYPE = "firestore_client_raw";
// TODO(metrics): change to firestore.googleapis.com
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";
+ String FIRESTORE_SERVICE = "firestore v1";
+
+ // Monitored resource
+ // TODO: check the monitored resource type with jimit
+ String FIRESTORE_RESOURCE_TYPE =
+ // "datastore_request";
+ // "datastore_client_raw";
+ "firestore.googleapis.com/Database";
String RESOURCE_KEY_LOCATION = "location";
- String RESOURCE_KEY_DATABASE_ID = "database_id";
+ String RESOURCE_KEY_INSTANCE = "instance";
+ String RESOURCE_KEY_DATABASE = "database";
+ String RESOURCE_KEY_PROJECT = "project";
+ String RESOURCE_KEY_UID = "uid";
+
Set FIRESTORE_RESOURCE_LABELS =
ImmutableSet.of(
- RESOURCE_KEY_RESOURCE_CONTAINER, RESOURCE_KEY_LOCATION, RESOURCE_KEY_DATABASE_ID);
+ RESOURCE_KEY_LOCATION,
+ RESOURCE_KEY_INSTANCE,
+ RESOURCE_KEY_DATABASE,
+ RESOURCE_KEY_PROJECT,
+ RESOURCE_KEY_UID);
// Metric attribute keys for labels
- AttributeKey METRIC_ATTRIBUTE_KEY_METHOD = AttributeKey.stringKey("method");
- AttributeKey METRIC_ATTRIBUTE_KEY_STATUS = AttributeKey.stringKey("status");
- AttributeKey METRIC_ATTRIBUTE_KEY_LIBRARY_NAME = AttributeKey.stringKey("library_name");
- AttributeKey METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION =
- AttributeKey.stringKey("library_version");
- AttributeKey METRIC_ATTRIBUTE_KEY_CLIENT_UID = AttributeKey.stringKey("client_uid");
- Set COMMON_ATTRIBUTES =
+ String METRIC_ATTRIBUTE_KEY_METHOD = "method";
+ String METRIC_ATTRIBUTE_KEY_STATUS = "status";
+ String METRIC_ATTRIBUTE_KEY_CLIENT_UID = "client_uid";
+ String METRIC_ATTRIBUTE_KEY_SERVICE = "service";
+ Set COMMON_ATTRIBUTES =
ImmutableSet.of(
METRIC_ATTRIBUTE_KEY_CLIENT_UID,
- METRIC_ATTRIBUTE_KEY_LIBRARY_NAME,
- METRIC_ATTRIBUTE_KEY_LIBRARY_VERSION,
METRIC_ATTRIBUTE_KEY_STATUS,
- METRIC_ATTRIBUTE_KEY_METHOD);
+ METRIC_ATTRIBUTE_KEY_METHOD,
+ METRIC_ATTRIBUTE_KEY_SERVICE,
+ RESOURCE_KEY_LOCATION,
+ RESOURCE_KEY_INSTANCE,
+ RESOURCE_KEY_DATABASE,
+ RESOURCE_KEY_PROJECT,
+ RESOURCE_KEY_UID);
// Metric names
String METRIC_NAME_OPERATION_LATENCY = "operation_latency";
@@ -96,7 +105,6 @@ public interface TelemetryConstants {
String METRIC_NAME_ATTEMPT_LATENCY = "attempt_latency";
String METRIC_NAME_ATTEMPT_COUNT = "attempt_count";
String METRIC_NAME_FIRST_RESPONSE_LATENCY = "first_response_latency";
- String METRIC_NAME_END_TO_END_LATENCY = "end_to_end_latency";
String METRIC_NAME_TRANSACTION_LATENCY = "transaction_latency";
String METRIC_NAME_TRANSACTION_ATTEMPT_COUNT = "transaction_attempt_count";
@@ -111,12 +119,10 @@ public interface TelemetryConstants {
Set FIRESTORE_METRICS =
ImmutableSet.of(
METRIC_NAME_FIRST_RESPONSE_LATENCY,
- METRIC_NAME_END_TO_END_LATENCY,
METRIC_NAME_TRANSACTION_LATENCY,
METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
public enum MetricType {
- END_TO_END_LATENCY,
FIRST_RESPONSE_LATENCY,
TRANSACTION_LATENCY,
TRANSACTION_ATTEMPT_COUNT
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ConformanceTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ConformanceTest.java
index 8b759e10d..517f77552 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ConformanceTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ConformanceTest.java
@@ -196,7 +196,14 @@ protected BaseConformanceTestRunner(final String description, final T testParame
this.firestore =
Mockito.spy(
new FirestoreImpl(
- FirestoreOptions.newBuilder().setProjectId("projectID").build(), firestoreRpc));
+ FirestoreOptions.newBuilder()
+ .setProjectId("projectID")
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build())
+ .build(),
+ firestoreRpc));
}
@Override
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
index 52ed3dc11..776005abe 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITBaseTest.java
@@ -22,6 +22,7 @@
import com.google.api.gax.rpc.TransportChannelProvider;
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.FirestoreSpy;
import com.google.cloud.firestore.ListenerRegistration;
@@ -55,6 +56,12 @@ public abstract class ITBaseTest {
public void before() throws Exception {
FirestoreOptions.Builder optionsBuilder = FirestoreOptions.newBuilder();
+ // disable the OpenTelemetry monitoring data for tests
+ optionsBuilder.setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build());
+
String dbPropertyName = "FIRESTORE_NAMED_DATABASE";
String namedDb = System.getProperty(dbPropertyName);
if (namedDb == null) {
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2EMetricsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2EMetricsTest.java
new file mode 100644
index 000000000..fe615b0a8
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2EMetricsTest.java
@@ -0,0 +1,322 @@
+/*
+ * 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.assertWithMessage;
+
+import com.google.cloud.firestore.Firestore;
+import com.google.cloud.firestore.FirestoreOpenTelemetryOptions;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.cloud.firestore.telemetry.TelemetryConstants;
+import com.google.cloud.monitoring.v3.MetricServiceClient;
+import com.google.common.collect.ImmutableSet;
+import com.google.monitoring.v3.ListTimeSeriesRequest;
+import com.google.monitoring.v3.ListTimeSeriesResponse;
+import com.google.monitoring.v3.TimeInterval;
+import com.google.monitoring.v3.TimeSeries;
+import com.google.protobuf.util.Timestamps;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+import org.threeten.bp.Instant;
+
+@RunWith(JUnit4.class)
+public class ITE2EMetricsTest extends ITBaseTest {
+
+ private static MetricServiceClient metricClient;
+
+ private static final Logger logger = Logger.getLogger(ITE2EMetricsTest.class.getName());
+
+ private static boolean isNightlyTesting;
+ private static String projectId;
+
+ private static Firestore firestore;
+
+ private FirestoreOptions.Builder optionsBuilder;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ String jobType = System.getenv("GITHUB_ENV_VAR_KOKORO_JOB_TYPE");
+ isNightlyTesting = jobType != null && jobType.equalsIgnoreCase("nightly");
+
+ projectId = FirestoreOptions.getDefaultProjectId();
+ logger.info("projectId:" + projectId);
+ }
+
+ @Before
+ public void before() throws Exception {
+ metricClient = MetricServiceClient.create();
+
+ optionsBuilder = FirestoreOptions.newBuilder();
+
+ String namedDb = System.getProperty("FIRESTORE_NAMED_DATABASE");
+ if (namedDb != null) {
+ logger.log(Level.INFO, "Integration test using named database " + namedDb);
+ optionsBuilder = optionsBuilder.setDatabaseId(namedDb);
+ } else {
+ logger.log(Level.INFO, "Integration test using default database.");
+ }
+
+ // These end-to-end metrics tests are resource-intensive and are only intended to run in a
+ // nightly testing environment.
+ // assumeTrue(isNightlyTesting);
+ }
+
+ @After
+ public void after() throws Exception {
+ if (firestore != null) {
+ // Shutting down Firestore can trigger a final export of metrics, potentially leading to
+ // "frequent write" errors in Cloud Monitoring. This sleep attempts to mitigate this by
+ // allowing a brief pause.
+ Thread.sleep(Duration.ofSeconds(30).toMillis());
+ firestore.shutdown();
+ }
+ metricClient.shutdown();
+ }
+
+ @Test
+ public void builtinMetricsWithDefaultOTEL() throws Exception {
+ firestore = optionsBuilder.build().getService();
+ TimeInterval interval = createTimeInterval();
+
+ firestore.collection("col").get().get();
+
+ Set METRICS =
+ ImmutableSet.of(
+ TelemetryConstants.METRIC_NAME_OPERATION_LATENCY,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY,
+ TelemetryConstants.METRIC_NAME_OPERATION_COUNT,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT,
+ TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+
+ // Verify metric data are published to Cloud Monitoring
+ for (String METRIC : METRICS) {
+ assertMetricsArePublished(METRIC, interval);
+ }
+ }
+
+ @Test
+ public void builtinMetricsWithDefaultAndCustomOTEL() throws Exception {
+ InMemoryMetricReader metricReader = InMemoryMetricReader.create();
+ SdkMeterProviderBuilder meterProvider =
+ SdkMeterProvider.builder().registerMetricReader(metricReader);
+ OpenTelemetrySdk customOpenTelemetrySdk =
+ OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
+ firestore =
+ optionsBuilder
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .setOpenTelemetry(customOpenTelemetrySdk)
+ .build())
+ .build()
+ .getService();
+ TimeInterval interval = createTimeInterval();
+
+ firestore.collection("col").count().get().get();
+
+ // Verify metric data are published to Cloud Monitoring
+ Set METRICS =
+ ImmutableSet.of(
+ TelemetryConstants.METRIC_NAME_OPERATION_LATENCY,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY,
+ TelemetryConstants.METRIC_NAME_OPERATION_COUNT,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT,
+ TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+
+ for (String METRIC : METRICS) {
+ assertMetricsArePublished(METRIC, interval);
+ }
+
+ // Verify metric data are collected to 3rd party backend (InMemoryMetricReader)
+ METRICS =
+ ImmutableSet.of(
+ TelemetryConstants.METRIC_NAME_OPERATION_LATENCY,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY,
+ TelemetryConstants.METRIC_NAME_OPERATION_COUNT,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT,
+ TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ for (String METRIC : METRICS) {
+ assertMetricsAreCollected(metricReader, METRIC);
+ }
+
+ // Verify no transaction related metric data are collected
+ METRICS =
+ ImmutableSet.of(
+ TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY,
+ TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+
+ for (String METRIC : METRICS) {
+ assertMetricsAreAbsent(metricReader, METRIC);
+ }
+ metricReader.shutdown();
+ }
+
+ @Test
+ public void builtinMetricsCreatedByTransaction() throws Exception {
+ firestore = optionsBuilder.build().getService();
+ TimeInterval interval = createTimeInterval();
+
+ firestore
+ .runTransaction(
+ transaction -> {
+ transaction.get(firestore.collection("col")).get();
+ transaction.set(
+ firestore.collection("foo").document("bar"),
+ Collections.singletonMap("foo", "bar"));
+ return 0;
+ })
+ .get();
+
+ Set METRICS =
+ ImmutableSet.of(
+ TelemetryConstants.METRIC_NAME_OPERATION_LATENCY,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY,
+ TelemetryConstants.METRIC_NAME_OPERATION_COUNT,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT,
+ TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY,
+ TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY,
+ TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+
+ // Verify metric data are published to Cloud Monitoring
+ for (String METRIC : METRICS) {
+ assertMetricsArePublished(METRIC, interval);
+ }
+ }
+
+ private TimeInterval createTimeInterval() {
+ Instant startTime = Instant.now().minus(Duration.ofSeconds(10));
+ // Set a wider time interval to make sure SDK has finished the batching (60s) and exporting of
+ // the metrics
+ Instant endTime = Instant.now().plus(Duration.ofSeconds(90));
+ return TimeInterval.newBuilder()
+ .setStartTime(Timestamps.fromMillis(startTime.toEpochMilli()))
+ .setEndTime(Timestamps.fromMillis(endTime.toEpochMilli()))
+ .build();
+ }
+
+ private void assertMetricsArePublished(String metric, TimeInterval interval) throws Exception {
+ String metricFilter =
+ String.format("metric.type=\"%s/%s\"", TelemetryConstants.METRIC_PREFIX, metric);
+ ListTimeSeriesRequest request =
+ ListTimeSeriesRequest.newBuilder()
+ .setName("projects/" + projectId)
+ .setFilter(metricFilter)
+ .setInterval(interval)
+ .build();
+
+ List filteredData = Collections.emptyList();
+ // Metrics are batched and exported periodically in the SDK (e.g., every 60s). And there is a
+ // potential delay between when the metric is published by the client library and when it
+ // becomes available. So we retry the fetching for multiple times with specified interval, to
+ // allow the monitoring system to catch up and for the data to become available.
+ int maxAttempts = 5;
+ Duration retryDelay = Duration.ofSeconds(30);
+
+ for (int attempt = 0; attempt < maxAttempts; attempt++) {
+ // Query Cloud Monitoring for the specific metric within the test's time window.
+ ListTimeSeriesResponse response = metricClient.listTimeSeriesCallable().call(request);
+
+ // In the testing environment where multiple tests might be running, the initial
+ // response could contain data from previous test runs. Filter the response to
+ // ensure we're validating the metrics generated by this specific test run and
+ // avoid interference from older data.
+ filteredData =
+ response.getTimeSeriesList().stream()
+ .filter(
+ ts ->
+ ts.getPoints(0).getInterval().getStartTime().getSeconds()
+ >= interval.getStartTime().getSeconds())
+ .collect(Collectors.toList());
+ if (!filteredData.isEmpty()) {
+ break;
+ }
+ logger.info(
+ "Retry fetching metrics data from Cloud Monitoring after "
+ + retryDelay.getSeconds()
+ + " seconds.");
+ Thread.sleep(retryDelay.toMillis());
+ }
+
+ assertWithMessage("Metric " + metric + " didn't return any data.")
+ .that(filteredData.size())
+ .isGreaterThan(0);
+ }
+
+ private void assertMetricsAreCollected(InMemoryMetricReader metricReader, String metricName) {
+ String fullMetricName = TelemetryConstants.METRIC_PREFIX + "/" + metricName;
+ List matchingMetadata =
+ metricReader.collectAllMetrics().stream()
+ .filter(md -> md.getName().equals(fullMetricName))
+ .collect(Collectors.toList());
+
+ // Fetch the MetricData with retries
+ int attemptsMade = 0;
+ while (matchingMetadata.size() == 0 && attemptsMade < 10) {
+ // Fetch metric data every seconds
+ try {
+ Thread.sleep(Duration.ofSeconds(1).toMillis());
+ } catch (InterruptedException interruptedException) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(interruptedException);
+ }
+ matchingMetadata =
+ metricReader.collectAllMetrics().stream()
+ .filter(md -> md.getName().equals(fullMetricName))
+ .collect(Collectors.toList());
+ attemptsMade++;
+ }
+
+ assertWithMessage(
+ "Found unexpected MetricData with the same name: %s, in: %s",
+ fullMetricName, matchingMetadata)
+ .that(matchingMetadata.size())
+ .isAtMost(1);
+
+ assertWithMessage("MetricData is missing for metric %s", fullMetricName)
+ .that(matchingMetadata.size())
+ .isEqualTo(1);
+ }
+
+ private void assertMetricsAreAbsent(InMemoryMetricReader metricReader, String metricName) {
+ String fullMetricName = TelemetryConstants.METRIC_PREFIX + "/" + metricName;
+ List matchingMetadata =
+ metricReader.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())
+ .isEqualTo(0);
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java
index b9c05ff2f..20e29d033 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITE2ETracingTest.java
@@ -323,6 +323,7 @@ public void before() throws Exception {
optionsBuilder.setOpenTelemetryOptions(
FirestoreOpenTelemetryOptions.newBuilder()
.setOpenTelemetry(openTelemetrySdk)
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
.build());
}
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..29819cc5f
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITMetricsTest.java
@@ -0,0 +1,710 @@
+/*
+ * 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.cloud.firestore.LocalFirestoreHelper.map;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.gax.grpc.GrpcStatusCode;
+import com.google.api.gax.rpc.ApiException;
+import com.google.api.gax.rpc.ApiStreamObserver;
+import com.google.cloud.firestore.*;
+import com.google.cloud.firestore.telemetry.ClientIdentifier;
+import com.google.cloud.firestore.telemetry.TelemetryConstants;
+import com.google.common.base.Preconditions;
+import io.grpc.Status;
+import io.opentelemetry.api.common.AttributeKey;
+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.HistogramPointData;
+import io.opentelemetry.sdk.metrics.data.LongPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.data.MetricDataType;
+import io.opentelemetry.sdk.metrics.data.PointData;
+import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+import org.threeten.bp.Duration;
+
+public class ITMetricsTest {
+ private static OpenTelemetrySdk openTelemetrySdk;
+
+ protected InMemoryMetricReader metricReader;
+
+ protected Firestore firestore;
+
+ private static Attributes baseAttributes;
+
+ private final String ClientUid = ClientIdentifier.getClientUid();
+
+ @Rule public TestName testName = new TestName();
+
+ @Before
+ public void setup() {
+ metricReader = InMemoryMetricReader.create();
+ openTelemetrySdk = setupOpenTelemetrySdk();
+ firestore = setupFirestoreService();
+ Preconditions.checkNotNull(
+ firestore,
+ "Error instantiating Firestore. Check that the service account credentials were properly"
+ + " set.");
+ baseAttributes = buildBaseAttributes();
+ }
+
+ private OpenTelemetrySdk setupOpenTelemetrySdk() {
+ SdkMeterProviderBuilder meterProvider =
+ SdkMeterProvider.builder().registerMetricReader(metricReader);
+ return OpenTelemetrySdk.builder().setMeterProvider(meterProvider.build()).build();
+ }
+
+ private Firestore setupFirestoreService() {
+ return FirestoreOptions.newBuilder()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setOpenTelemetry(openTelemetrySdk).build())
+ .build()
+ .getService();
+ }
+
+ private Attributes buildBaseAttributes() {
+ AttributesBuilder attributesBuilder = Attributes.builder();
+ attributesBuilder.put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_CLIENT_UID, ClientUid);
+ attributesBuilder.put(
+ TelemetryConstants.METRIC_ATTRIBUTE_KEY_SERVICE, TelemetryConstants.FIRESTORE_SERVICE);
+
+ return attributesBuilder.build();
+ }
+
+ @After
+ public void tearDown() {
+ try {
+ openTelemetrySdk.getSdkMeterProvider().shutdown();
+ } finally {
+ firestore.shutdown();
+ }
+ }
+
+ class MetricInfo {
+ // The expected number of measurements collected
+ public int count;
+ // Attributes expected to be recorded in the measurements
+ public Attributes attributes;
+
+ public MetricInfo(int expectedCount, Attributes expectedAttributes) {
+ this.count = expectedCount;
+ this.attributes = expectedAttributes;
+ }
+ }
+
+ class MetricsExpectationBuilder {
+ private final Map expectedMetrics = new HashMap<>();
+
+ public MetricsExpectationBuilder expectMetricData(
+ String method, String expectedStatus, int expectedCount) {
+ Attributes attributes = buildAttributes(method, expectedStatus);
+ expectedMetrics.put(method, new MetricInfo(expectedCount, attributes));
+ return this;
+ }
+
+ public Map build() {
+ return expectedMetrics;
+ }
+ }
+
+ @Test
+ public void queryGet() throws Exception {
+ firestore.collection("col").get().get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.RunQuery", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_QUERY_GET, Status.OK.getCode().toString(), 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void queryExplain() throws Exception {
+ Query query = firestore.collection("col");
+ ApiFuture metricsFuture =
+ query.explainStream(
+ ExplainOptions.builder().setAnalyze(false).build(),
+ new ApiStreamObserver() {
+ @Override
+ public void onNext(DocumentSnapshot documentSnapshot) {
+ fail("No DocumentSnapshot should be received because analyze option was disabled.");
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ fail(throwable.getMessage());
+ }
+
+ @Override
+ public void onCompleted() {}
+ });
+
+ ExplainMetrics metrics = metricsFuture.get();
+ assertThat(metrics.getPlanSummary().getIndexesUsed().size()).isGreaterThan(0);
+ assertThat(metrics.getExecutionStats()).isNull();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.RunQuery", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_QUERY_EXPLAIN, Status.OK.getCode().toString(), 1)
+ .build();
+
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void aggregateQueryGet() throws Exception {
+ firestore.collection("col").count().get().get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.RunAggregationQuery", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_GET,
+ Status.OK.getCode().toString(),
+ 1)
+ .build();
+
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void writeBatch() throws Exception {
+ WriteBatch batch = firestore.batch();
+ DocumentReference docRef = firestore.collection("foo").document();
+ batch.create(docRef, Collections.singletonMap("foo", "bar"));
+ batch.update(docRef, Collections.singletonMap("foo", "bar"));
+ batch.delete(docRef);
+ batch.commit().get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.Commit", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void bulkWriterCommit() throws Exception {
+ ScheduledExecutorService bulkWriterExecutor = Executors.newSingleThreadScheduledExecutor();
+ BulkWriter bulkWriter =
+ firestore.bulkWriter(BulkWriterOptions.builder().setExecutor(bulkWriterExecutor).build());
+ bulkWriter.set(
+ firestore.collection("col").document("foo"),
+ Collections.singletonMap("bulk-foo", "bulk-bar"));
+ bulkWriter.close();
+ bulkWriterExecutor.awaitTermination(100, TimeUnit.MILLISECONDS);
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.BatchWrite", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void partitionQuery() throws Exception {
+ CollectionGroup collectionGroup = firestore.collectionGroup("col");
+ collectionGroup.getPartitions(3).get();
+
+ // Note: pagedCallable requests are not traced at GAX layer
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void listCollection() throws Exception {
+ firestore.collection("col").document("doc0").listCollections();
+
+ // Note: pagedCallable requests are not traced at GAX layer
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void collectionListDocuments() throws Exception {
+ firestore.collection("col").listDocuments();
+
+ // Note: pagedCallable requests are not traced at GAX layer
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void docRefSet() throws Exception {
+ firestore
+ .collection("col")
+ .document("foo")
+ .set(Collections.singletonMap("foo", "bar"), SetOptions.merge())
+ .get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.Commit", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void getAll() throws Exception {
+ DocumentReference docRef0 = firestore.collection("col").document();
+ DocumentReference docRef1 = firestore.collection("col").document();
+ DocumentReference[] docs = {docRef0, docRef1};
+ firestore.getAll(docs).get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.BatchGetDocuments", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_BATCH_GET_DOCUMENTS,
+ Status.OK.getCode().toString(),
+ 1)
+ .build();
+
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ @Test
+ public void transaction() throws Exception {
+ firestore
+ .runTransaction( // Has end-to-end, first response latency and transaction metrics.
+ transaction -> {
+ Query q = firestore.collection("col").whereGreaterThan("bla", "");
+ // Document Query. Has end-to-end and first response latency which is marked
+ // transactional
+ transaction.get(q).get();
+
+ // Aggregation Query. Has end-to-end and first response latency which is marked
+ // transactional
+ transaction.get(q.count());
+
+ // Commit 2 documents. Has end-to-end latency.
+ 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();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.BeginTransaction", Status.OK.getCode().toString(), 1)
+ .expectMetricData("Firestore.RunQuery", Status.OK.getCode().toString(), 1)
+ .expectMetricData("Firestore.RunAggregationQuery", Status.OK.getCode().toString(), 1)
+ .expectMetricData("Firestore.Commit", Status.OK.getCode().toString(), 1)
+ .build();
+
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 1)
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_TRANSACTION_GET_QUERY,
+ Status.OK.getCode().toString(),
+ 1)
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_TRANSACTION_GET_AGGREGATION_QUERY,
+ Status.OK.getCode().toString(),
+ 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY, expectedMetrics);
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT, expectedMetrics);
+ }
+
+ @Test
+ public void transactionWithRollback() throws Exception {
+ String myErrorMessage = "My error message.";
+ try {
+ firestore
+ .runTransaction(
+ // BeginTransaction is successful, RunTransaction receives its first response.
+ transaction -> {
+ if (true) {
+ // Transaction encounters not retryable error, ends with failure
+ throw (new Exception(myErrorMessage));
+ }
+ return 0;
+ })
+ .get();
+ } catch (Exception e) {
+ // Catch and move on.
+ }
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.BeginTransaction", Status.OK.getCode().toString(), 1)
+ .expectMetricData("Firestore.Rollback", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION,
+ Status.UNKNOWN.getCode().toString(),
+ 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY, expectedMetrics);
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT, expectedMetrics);
+ }
+
+ @Test
+ public void transactionWithFailure() throws Exception {
+ Firestore invalidFirestore =
+ FirestoreOptions.newBuilder()
+ .setDatabaseId("foo.bar") // Invalid databaseId
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .setOpenTelemetry(openTelemetrySdk)
+ .build())
+ .build()
+ .getService();
+
+ try {
+ invalidFirestore
+ .runTransaction(
+ // Transaction cannot get started due to the invalid database ID
+ transaction -> {
+ return 0;
+ })
+ .get();
+ } catch (Exception e) {
+ // Catch and move on.
+ }
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ "Firestore.BeginTransaction", Status.NOT_FOUND.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION,
+ Status.NOT_FOUND.getCode().toString(),
+ 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY, expectedMetrics);
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT, expectedMetrics);
+ }
+
+ @Test
+ public void transactionWithRetry() throws Exception {
+ // Throw a retryable error
+ ApiException RETRYABLE_API_EXCEPTION =
+ new ApiException(
+ new Exception("Test exception"), GrpcStatusCode.of(Status.Code.UNKNOWN), true);
+
+ try {
+ firestore
+ .runTransaction(
+ // RunTransaction retries from BeginTransaction for 3 times
+ new Transaction.Function() {
+ int cnt = 1;
+
+ @Override
+ public Integer updateCallback(Transaction transaction) throws Exception {
+ cnt++;
+ if (cnt <= 3) {
+ throw RETRYABLE_API_EXCEPTION;
+ }
+ return 0;
+ }
+ })
+ .get();
+ } catch (Exception e) {
+ // Catch and move on.
+ }
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.BeginTransaction", Status.OK.getCode().toString(), 3)
+ .expectMetricData("Firestore.Rollback", Status.OK.getCode().toString(), 2)
+ .expectMetricData("Firestore.Commit", Status.OK.getCode().toString(), 1)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY, expectedMetrics);
+
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_RUN_TRANSACTION, Status.OK.getCode().toString(), 3)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT, expectedMetrics);
+ }
+
+ @Test
+ public void multipleOperations() throws Exception {
+ // 2 get query
+ firestore.collection("col1").get().get();
+ firestore.collection("col2").get().get();
+ // 1 aggregation get query
+ firestore.collection("col").count().get().get();
+ // 3 batch commits on document reference: set, update,delete
+ DocumentReference ref = firestore.collection("col").document("doc1");
+ ref.set(Collections.singletonMap("foo", "bar")).get();
+ ref.update(map("foo", "newBar")).get();
+ ref.delete().get();
+
+ // Validate GAX layer metrics
+ Map expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData("Firestore.RunQuery", Status.OK.getCode().toString(), 2)
+ .expectMetricData("Firestore.RunAggregationQuery", Status.OK.getCode().toString(), 1)
+ .expectMetricData("Firestore.Commit", Status.OK.getCode().toString(), 3)
+ .build();
+ validateGaxMetrics(expectedMetrics);
+
+ // Validate SDK layer metric
+ expectedMetrics =
+ new MetricsExpectationBuilder()
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_QUERY_GET, Status.OK.getCode().toString(), 2)
+ .expectMetricData(
+ TelemetryConstants.METHOD_NAME_AGGREGATION_QUERY_GET,
+ Status.OK.getCode().toString(),
+ 1)
+ .build();
+ validateSDKMetrics(TelemetryConstants.METRIC_NAME_FIRST_RESPONSE_LATENCY, expectedMetrics);
+
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_LATENCY);
+ assertMetricAbsent(TelemetryConstants.METRIC_NAME_TRANSACTION_ATTEMPT_COUNT);
+ }
+
+ private Attributes buildAttributes(String method, String status) {
+ return baseAttributes.toBuilder()
+ .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS, status)
+ .put(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD, method)
+ .build();
+ }
+
+ private void validateSDKMetrics(String metric, Map expectedMetrics) {
+ MetricData dataFromReader = getMetricData(metric);
+ validateMetricData(dataFromReader, expectedMetrics);
+ }
+
+ private void validateGaxMetrics(Map expectedMetrics) {
+ List gaxMetricNames =
+ Arrays.asList(
+ TelemetryConstants.METRIC_NAME_ATTEMPT_LATENCY,
+ TelemetryConstants.METRIC_NAME_ATTEMPT_COUNT,
+ TelemetryConstants.METRIC_NAME_OPERATION_COUNT,
+ TelemetryConstants.METRIC_NAME_OPERATION_LATENCY);
+ for (String metricName : gaxMetricNames) {
+ MetricData dataFromReader = getMetricData(metricName);
+ validateMetricData(dataFromReader, expectedMetrics);
+ }
+ }
+
+ private void validateMetricData(MetricData metricData, Map expectedMetrics) {
+ boolean isHistogram = metricData.getType() == MetricDataType.HISTOGRAM;
+ Collection extends PointData> points =
+ isHistogram
+ ? metricData.getHistogramData().getPoints()
+ : metricData.getLongSumData().getPoints();
+ assertThat(points.size()).isEqualTo(expectedMetrics.size());
+
+ for (PointData point : points) {
+ String method =
+ point
+ .getAttributes()
+ .get(AttributeKey.stringKey(TelemetryConstants.METRIC_ATTRIBUTE_KEY_METHOD));
+ MetricInfo expectedMetricInfo = expectedMetrics.get(method);
+ if (isHistogram) {
+ assertThat(((HistogramPointData) point).getCount()).isEqualTo(expectedMetricInfo.count);
+ assertThat(((HistogramPointData) point).getSum()).isGreaterThan(0.0);
+ } else {
+ assertThat(((LongPointData) point).getValue()).isEqualTo(expectedMetricInfo.count);
+ }
+ assertThat(point.getAttributes().asMap())
+ .containsAtLeastEntriesIn(expectedMetricInfo.attributes.asMap());
+ }
+ }
+
+ private MetricData getMetricData(String metricName) {
+ String fullMetricName = TelemetryConstants.METRIC_PREFIX + "/" + metricName;
+ // Fetch the MetricData with retries
+ for (int attemptsLeft = 10; attemptsLeft > 0; attemptsLeft--) {
+ List matchingMetadata =
+ metricReader.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);
+
+ // Tests could be flaky as the metric reader could have matching data, but it is partial.
+ if (!matchingMetadata.isEmpty()) {
+ return matchingMetadata.get(0);
+ }
+
+ try {
+ Thread.sleep(Duration.ofSeconds(1).toMillis());
+ } catch (InterruptedException interruptedException) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(interruptedException);
+ }
+ }
+
+ assertTrue(String.format("MetricData is missing for metric %s", fullMetricName), false);
+ return null;
+ }
+
+ private void assertMetricAbsent(String metricName) {
+ String fullMetricName = TelemetryConstants.METRIC_PREFIX + "/" + metricName;
+ List matchingMetadata =
+ metricReader.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())
+ .isEqualTo(0);
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java
index 7efdd0704..7e304035c 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITTracingTest.java
@@ -111,7 +111,8 @@ public void before() {
SpanProcessor inMemorySpanProcessor = SimpleSpanProcessor.create(inMemorySpanExporter);
FirestoreOptions.Builder optionsBuilder = FirestoreOptions.newBuilder();
FirestoreOpenTelemetryOptions.Builder otelOptionsBuilder =
- FirestoreOpenTelemetryOptions.newBuilder();
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false);
OpenTelemetrySdkBuilder openTelemetrySdkBuilder =
OpenTelemetrySdk.builder()
.setTracerProvider(
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpcTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpcTest.java
index e4e888ff0..35ebe67c5 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpcTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/spi/v1/GrpcFirestoreRpcTest.java
@@ -26,6 +26,7 @@
import com.google.api.gax.rpc.ServerStreamingCallSettings;
import com.google.api.gax.rpc.StatusCode.Code;
import com.google.api.gax.rpc.UnaryCallSettings;
+import com.google.cloud.firestore.FirestoreOpenTelemetryOptions;
import com.google.cloud.firestore.FirestoreOptions;
import com.google.cloud.firestore.v1.FirestoreClient.ListDocumentsPagedResponse;
import com.google.cloud.firestore.v1.FirestoreClient.PartitionQueryPagedResponse;
@@ -59,7 +60,13 @@ public class GrpcFirestoreRpcTest {
private static FirestoreStubSettings defaultStubSettings;
private final FirestoreOptions firestoreOptionsWithoutOverride =
- FirestoreOptions.newBuilder().setProjectId("test-project").build();
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build())
+ .build();
@BeforeClass
public static void beforeClass() throws IOException {
@@ -85,6 +92,10 @@ public void retrySettingsOverride() throws Exception {
FirestoreOptions.newBuilder()
.setProjectId("test-project")
.setRetrySettings(retrySettings)
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build())
.build();
GrpcFirestoreRpc grpcFirestoreRpc = new GrpcFirestoreRpc(firestoreOptions);
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProviderTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProviderTest.java
new file mode 100644
index 000000000..92a95e068
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/BuiltinMetricsProviderTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.telemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.*;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class BuiltinMetricsProviderTest {
+ private OpenTelemetry mockOpenTelemetry;
+ private Meter mockMeter;
+ private DoubleHistogram mockFirstResponseLatency;
+ private DoubleHistogram mockTransactionLatency;
+ private LongCounter mockTransactionAttemptCount;
+ private BuiltinMetricsProvider metricsProvider;
+
+ @Before
+ public void setUp() {
+ // Mock OpenTelemetry components
+ mockOpenTelemetry = mock(OpenTelemetry.class);
+ MeterProvider mockMeterProvider = mock(MeterProvider.class);
+ when(mockOpenTelemetry.getMeterProvider()).thenReturn(mockMeterProvider);
+ mockMeter = mock(Meter.class);
+ when(mockOpenTelemetry.getMeter(anyString())).thenReturn(mockMeter);
+ MeterBuilder mockMeterBuilder = mock(MeterBuilder.class);
+ when(mockMeterBuilder.setInstrumentationVersion(anyString())).thenReturn(mockMeterBuilder);
+ when(mockMeterBuilder.build()).thenReturn(mockMeter);
+ when(mockOpenTelemetry.meterBuilder(anyString())).thenReturn(mockMeterBuilder);
+
+ // Mock Histogram and Counter builders
+ DoubleHistogramBuilder mockHistogramBuilder = mock(DoubleHistogramBuilder.class);
+ when(mockMeter.histogramBuilder(anyString())).thenReturn(mockHistogramBuilder);
+ when(mockHistogramBuilder.setDescription(anyString())).thenReturn(mockHistogramBuilder);
+ when(mockHistogramBuilder.setUnit(anyString())).thenReturn(mockHistogramBuilder);
+ LongCounterBuilder mockCounterBuilder = mock(LongCounterBuilder.class);
+ when(mockMeter.counterBuilder(anyString())).thenReturn(mockCounterBuilder);
+ when(mockCounterBuilder.setDescription(anyString())).thenReturn(mockCounterBuilder);
+ when(mockCounterBuilder.setUnit(anyString())).thenReturn(mockCounterBuilder);
+
+ mockFirstResponseLatency = mock(DoubleHistogram.class);
+ mockTransactionLatency = mock(DoubleHistogram.class);
+ mockTransactionAttemptCount = mock(LongCounter.class);
+
+ // Configure mockHistogramBuilder to return the first 2 histogram mocks created in sequence.
+ // The SDK configures 3 SDK layer metrics first, and then the RPC metrics gets created in GAX
+ // layer
+ when(mockHistogramBuilder.build())
+ .thenReturn(mockFirstResponseLatency)
+ .thenReturn(mockTransactionLatency);
+
+ // Mock the counter builder to return the transaction attempt counter
+ when(mockCounterBuilder.build()).thenReturn(mockTransactionAttemptCount);
+
+ // Set up BuiltinMetricsProvider instance with mocked OpenTelemetry
+ metricsProvider = new BuiltinMetricsProvider(mockOpenTelemetry);
+ }
+
+ @Test
+ public void SDKLayerMetricsConfiguredSuccessfully() {
+ metricsProvider = new BuiltinMetricsProvider(mockOpenTelemetry);
+ assertNotNull(metricsProvider.getHistogram(MetricType.FIRST_RESPONSE_LATENCY));
+ assertNotNull(metricsProvider.getHistogram(MetricType.TRANSACTION_LATENCY));
+ assertNotNull(metricsProvider.getCounter(MetricType.TRANSACTION_ATTEMPT_COUNT));
+ }
+
+ @Test
+ public void getHistogramReturnsHistogramsInstrumentCorrectly() {
+ DoubleHistogram mockHistogram = metricsProvider.getHistogram(MetricType.FIRST_RESPONSE_LATENCY);
+ assertEquals(mockFirstResponseLatency, mockHistogram);
+
+ mockHistogram = metricsProvider.getHistogram(MetricType.TRANSACTION_LATENCY);
+ assertEquals(mockTransactionLatency, mockHistogram);
+ }
+
+ @Test
+ public void getHistogramThrowsOnInvalidMetricType() {
+ Exception exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> metricsProvider.getHistogram(MetricType.TRANSACTION_ATTEMPT_COUNT));
+ assertEquals("Unknown latency MetricType: TRANSACTION_ATTEMPT_COUNT", exception.getMessage());
+ }
+
+ @Test
+ public void getCounterReturnsCounterInstrumentCorrectly() {
+ LongCounter mockCounter = metricsProvider.getCounter(MetricType.TRANSACTION_ATTEMPT_COUNT);
+ assertEquals(mockTransactionAttemptCount, mockCounter);
+ }
+
+ @Test
+ public void getCounterThrowsOnInvalidMetricType() {
+ Exception exception =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> metricsProvider.getCounter(MetricType.FIRST_RESPONSE_LATENCY));
+ assertEquals("Unknown counter MetricType: FIRST_RESPONSE_LATENCY", exception.getMessage());
+ }
+
+ @Test
+ public void latencyRecorderTriggersCorrectInstrument() {
+ Map attributes = new HashMap<>();
+ attributes.put("attribute", "value");
+
+ metricsProvider.latencyRecorder(MetricType.FIRST_RESPONSE_LATENCY, 50.0, attributes);
+ verify(mockFirstResponseLatency)
+ .record(
+ eq(50.0),
+ argThat(
+ arguments -> arguments.get(AttributeKey.stringKey("attribute")).equals("value")));
+
+ metricsProvider.latencyRecorder(MetricType.TRANSACTION_LATENCY, 200.0, attributes);
+ verify(mockTransactionLatency)
+ .record(
+ eq(200.0),
+ argThat(
+ arguments -> arguments.get(AttributeKey.stringKey("attribute")).equals("value")));
+ }
+
+ @Test
+ public void counterRecorderTriggersCorrectInstrument() {
+ Map attributes = new HashMap<>();
+ attributes.put("key", "value");
+
+ metricsProvider.counterRecorder(MetricType.TRANSACTION_ATTEMPT_COUNT, 5, attributes);
+ verify(mockTransactionAttemptCount).add(eq(5L), any(Attributes.class));
+ }
+
+ @Test
+ public void handlesNoopMetricProviderGracefully() throws Exception {
+ BuiltinMetricsProvider provider = new BuiltinMetricsProvider(OpenTelemetry.noop());
+ Map attributes = new HashMap<>();
+ attributes.put("key", "disabledTest");
+
+ try {
+ provider.latencyRecorder(MetricType.FIRST_RESPONSE_LATENCY, 100.0, attributes);
+ provider.latencyRecorder(MetricType.TRANSACTION_LATENCY, 150.0, attributes);
+ provider.counterRecorder(MetricType.TRANSACTION_ATTEMPT_COUNT, 1, attributes);
+ } catch (Exception e) {
+ assertThat(e).isNull(); // Fail the test if any exception is thrown
+ }
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/ClientIdentifierTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/ClientIdentifierTest.java
new file mode 100644
index 000000000..ca9418f94
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/ClientIdentifierTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.telemetry;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+public final class ClientIdentifierTest {
+
+ @Test
+ public void getClientUidGeneratesUidOnlyOnce() {
+ String firstUid = ClientIdentifier.getClientUid();
+ String secondUid = ClientIdentifier.getClientUid();
+ assertEquals(firstUid, secondUid);
+ }
+
+ @Test
+ public void generateClientUidHasExpectedFormat() {
+ String clientUid = ClientIdentifier.getClientUid();
+
+ String expectedPattern =
+ String.format(
+ "%s@%s@%s",
+ "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "\\d+", "[\\w.-]+");
+
+ assertTrue(clientUid.matches(expectedPattern));
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/CompositeApiTracerTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/CompositeApiTracerTest.java
new file mode 100644
index 000000000..2b996d54e
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/CompositeApiTracerTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.telemetry;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import com.google.api.gax.tracing.ApiTracer;
+import java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.threeten.bp.Duration;
+
+@RunWith(JUnit4.class)
+public class CompositeApiTracerTest {
+
+ private final ApiTracer child1 = mock(ApiTracer.class);
+ private final ApiTracer child2 = mock(ApiTracer.class);
+ private final CompositeApiTracer compositeApiTracer =
+ new CompositeApiTracer(Arrays.asList(child1, child2));
+
+ @Test
+ public void inScope_callsInScopeOnChildren() {
+ compositeApiTracer.inScope();
+
+ verify(child1, times(1)).inScope();
+ verify(child2, times(1)).inScope();
+ }
+
+ @Test
+ public void inScope_closesChildScopes() {
+ ApiTracer.Scope childScope1 = mock(ApiTracer.Scope.class);
+ ApiTracer.Scope childScope2 = mock(ApiTracer.Scope.class);
+ when(child1.inScope()).thenReturn(childScope1);
+ when(child2.inScope()).thenReturn(childScope2);
+
+ compositeApiTracer.inScope().close();
+
+ verify(child1).inScope();
+ verify(child2).inScope();
+ verify(childScope1).close();
+ verify(childScope2).close();
+ }
+
+ @Test
+ public void operationSucceeded_callsOperationSucceededOnChildren() {
+ compositeApiTracer.operationSucceeded();
+
+ verify(child1, times(1)).operationSucceeded();
+ verify(child2, times(1)).operationSucceeded();
+ }
+
+ @Test
+ public void operationCancelled_callsOperationCancelledOnChildren() {
+ compositeApiTracer.operationCancelled();
+
+ verify(child1, times(1)).operationCancelled();
+ verify(child2, times(1)).operationCancelled();
+ }
+
+ @Test
+ public void operationFailed_callsOperationFailedOnChildren() {
+ Exception error = new Exception("Test error");
+ compositeApiTracer.operationFailed(error);
+
+ verify(child1, times(1)).operationFailed(error);
+ verify(child2, times(1)).operationFailed(error);
+ }
+
+ @Test
+ public void connectionSelected_callsConnectionSelectedOnChildren() {
+ String connectionId = "connection-id";
+ compositeApiTracer.connectionSelected(connectionId);
+
+ verify(child1, times(1)).connectionSelected(connectionId);
+ verify(child2, times(1)).connectionSelected(connectionId);
+ }
+
+ @Test
+ public void attemptStarted_callsAttemptStartedOnChildren() {
+ int attemptNumber = 1;
+ compositeApiTracer.attemptStarted(attemptNumber);
+
+ verify(child1, times(1)).attemptStarted(null, attemptNumber);
+ verify(child2, times(1)).attemptStarted(null, attemptNumber);
+ }
+
+ @Test
+ public void attemptStartedWithRequest_callsAttemptStartedOnChildren() {
+ Object request = new Object();
+ int attemptNumber = 1;
+ compositeApiTracer.attemptStarted(request, attemptNumber);
+
+ verify(child1, times(1)).attemptStarted(request, attemptNumber);
+ verify(child2, times(1)).attemptStarted(request, attemptNumber);
+ }
+
+ @Test
+ public void attemptSucceeded_callsAttemptSucceededOnChildren() {
+ compositeApiTracer.attemptSucceeded();
+
+ verify(child1, times(1)).attemptSucceeded();
+ verify(child2, times(1)).attemptSucceeded();
+ }
+
+ @Test
+ public void attemptCancelled_callsAttemptCancelledOnChildren() {
+ compositeApiTracer.attemptCancelled();
+
+ verify(child1, times(1)).attemptCancelled();
+ verify(child2, times(1)).attemptCancelled();
+ }
+
+ @Test
+ public void attemptFailedDuration_callsAttemptFailedDurationOnChildren() {
+ Exception error = new Exception("Test error");
+ java.time.Duration delay = java.time.Duration.ofSeconds(1);
+ compositeApiTracer.attemptFailedDuration(error, delay);
+
+ verify(child1, times(1)).attemptFailedDuration(error, delay);
+ verify(child2, times(1)).attemptFailedDuration(error, delay);
+ }
+
+ @Test
+ public void attemptFailedRetriesExhausted_callsAttemptFailedRetriesExhaustedOnChildren() {
+ Exception error = new Exception("Test error");
+ compositeApiTracer.attemptFailedRetriesExhausted(error);
+
+ verify(child1, times(1)).attemptFailedRetriesExhausted(error);
+ verify(child2, times(1)).attemptFailedRetriesExhausted(error);
+ }
+
+ @Test
+ public void attemptPermanentFailure_callsAttemptPermanentFailureOnChildren() {
+ Exception error = new Exception("Test error");
+ compositeApiTracer.attemptPermanentFailure(error);
+
+ verify(child1, times(1)).attemptPermanentFailure(error);
+ verify(child2, times(1)).attemptPermanentFailure(error);
+ }
+
+ @Test
+ public void lroStartFailed_callsLroStartFailedOnChildren() {
+ Exception error = new Exception("Test error");
+ compositeApiTracer.lroStartFailed(error);
+
+ verify(child1, times(1)).lroStartFailed(error);
+ verify(child2, times(1)).lroStartFailed(error);
+ }
+
+ @Test
+ public void lroStartSucceeded_callsLroStartSucceededOnChildren() {
+ compositeApiTracer.lroStartSucceeded();
+
+ verify(child1, times(1)).lroStartSucceeded();
+ verify(child2, times(1)).lroStartSucceeded();
+ }
+
+ @Test
+ public void responseReceived_callsResponseReceivedOnChildren() {
+ compositeApiTracer.responseReceived();
+
+ verify(child1, times(1)).responseReceived();
+ verify(child2, times(1)).responseReceived();
+ }
+
+ @Test
+ public void requestSent_callsRequestSentOnChildren() {
+ compositeApiTracer.requestSent();
+
+ verify(child1, times(1)).requestSent();
+ verify(child2, times(1)).requestSent();
+ }
+
+ @Test
+ public void batchRequestSent_callsBatchRequestSentOnChildren() {
+ long elementCount = 10;
+ long requestSize = 100;
+ compositeApiTracer.batchRequestSent(elementCount, requestSize);
+
+ verify(child1, times(1)).batchRequestSent(elementCount, requestSize);
+ verify(child2, times(1)).batchRequestSent(elementCount, requestSize);
+ }
+
+ @Test
+ public void noInteractions_whenNoChildren() {
+ CompositeApiTracer emptyTracer = new CompositeApiTracer(Arrays.asList());
+
+ emptyTracer.inScope();
+ emptyTracer.operationSucceeded();
+ emptyTracer.operationCancelled();
+ emptyTracer.operationFailed(new Exception("Test error"));
+ emptyTracer.connectionSelected("connection-id");
+ emptyTracer.attemptStarted(1);
+ emptyTracer.attemptStarted(new Object(), 1);
+ emptyTracer.attemptSucceeded();
+ emptyTracer.attemptCancelled();
+ emptyTracer.attemptFailed(new Exception("Test error"), Duration.ofSeconds(1));
+ emptyTracer.attemptFailedDuration(new Exception("Test error"), java.time.Duration.ofSeconds(1));
+ emptyTracer.attemptFailedRetriesExhausted(new Exception("Test error"));
+ emptyTracer.attemptPermanentFailure(new Exception("Test error"));
+ emptyTracer.lroStartFailed(new Exception("Test error"));
+ emptyTracer.lroStartSucceeded();
+ emptyTracer.responseReceived();
+ emptyTracer.requestSent();
+ emptyTracer.batchRequestSent(10, 100);
+
+ verifyNoMoreInteractions(child1, child2);
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledMetricsUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledMetricsUtilTest.java
new file mode 100644
index 000000000..877b6ef96
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledMetricsUtilTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.telemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.gax.tracing.ApiTracerFactory;
+import com.google.cloud.firestore.telemetry.DisabledMetricsUtil.MetricsContext;
+import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DisabledMetricsUtilTest {
+ private DisabledMetricsUtil disabledMetricsUtil;
+
+ @Before
+ public void setUp() {
+ disabledMetricsUtil = new DisabledMetricsUtil();
+ }
+
+ @Test
+ public void createMetricsContextShouldReturnNonNullContext() {
+ assertThat(disabledMetricsUtil.createMetricsContext("testMethod")).isNotNull();
+ assertThat(
+ disabledMetricsUtil.createMetricsContext("testMethod")
+ instanceof DisabledMetricsUtil.MetricsContext)
+ .isTrue();
+ }
+
+ @Test
+ public void shouldNotAddMetricsTracerFactory() {
+ List factories = new ArrayList<>();
+ disabledMetricsUtil.addMetricsTracerFactory(factories);
+ assertThat(factories).isEmpty();
+ }
+
+ @Test
+ public void shouldNotThrowOnMetricsCollection() {
+ MetricsContext context = disabledMetricsUtil.createMetricsContext("testMethod");
+
+ // Ensure no exceptions are thrown by no-op methods
+ try {
+ context.recordLatency(MetricType.FIRST_RESPONSE_LATENCY, new Exception("test"));
+ context.recordLatency(MetricType.FIRST_RESPONSE_LATENCY);
+ context.incrementCounter();
+
+ ApiFuture future = ApiFutures.immediateFuture("test");
+ context.recordLatencyAtFuture(MetricType.FIRST_RESPONSE_LATENCY, future);
+ } catch (Exception e) {
+ assertThat(e).isNull(); // Fail the test if any exception is thrown
+ }
+ }
+}
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
new file mode 100644
index 000000000..bdc7ab39b
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledMetricsUtilTest.java
@@ -0,0 +1,367 @@
+/*
+ * 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.telemetry;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.anyDouble;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutures;
+import com.google.api.gax.grpc.GrpcStatusCode;
+import com.google.api.gax.rpc.ApiException;
+import com.google.api.gax.rpc.StatusCode;
+import com.google.api.gax.tracing.ApiTracerFactory;
+import com.google.cloud.firestore.FirestoreException;
+import com.google.cloud.firestore.FirestoreOpenTelemetryOptions;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.cloud.firestore.telemetry.EnabledMetricsUtil.MetricsContext;
+import com.google.cloud.firestore.telemetry.TelemetryConstants.MetricType;
+import io.grpc.Status;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+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;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class EnabledMetricsUtilTest {
+
+ private BuiltinMetricsProvider defaultProvider;
+ private BuiltinMetricsProvider customProvider;
+
+ @Before
+ public void setUp() {
+ GlobalOpenTelemetry.resetForTest();
+ defaultProvider = Mockito.mock(BuiltinMetricsProvider.class);
+ customProvider = Mockito.mock(BuiltinMetricsProvider.class);
+ }
+
+ FirestoreOptions.Builder getBaseOptions() {
+ return FirestoreOptions.newBuilder().setProjectId("test-project").setDatabaseId("(default)");
+ }
+
+ EnabledMetricsUtil newEnabledMetricsUtil() {
+ return new EnabledMetricsUtil(getBaseOptions().build());
+ }
+
+ @Test
+ public void createsDefaultBuiltinMetricsProviderWithBuiltinOpenTelemetryInstance() {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ BuiltinMetricsProvider metricsProvider = metricsUtil.getDefaultMetricsProvider();
+ assertThat(metricsProvider).isNotNull();
+ assertThat(metricsProvider.getOpenTelemetry()).isNotNull();
+ // The default OpenTelemetry MeterProvider has registered GoogleCloudMonitoringExporter.
+ assertThat(metricsProvider.getOpenTelemetry().getMeterProvider())
+ .isNotEqualTo(MeterProvider.noop());
+ }
+
+ @Test
+ public void canDisableBuiltinMetricsProviderWithFirestoreOpenTelemetryOptions() {
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build())
+ .build();
+ EnabledMetricsUtil metricsUtil = new EnabledMetricsUtil(firestoreOptions);
+
+ BuiltinMetricsProvider metricsProvider = metricsUtil.getDefaultMetricsProvider();
+ assertThat(metricsProvider).isNotNull();
+ assertThat(metricsProvider.getOpenTelemetry()).isNotNull();
+ // Metrics collection is "disabled" with OpenTelemetry No-op meter provider instance
+ assertThat(metricsProvider.getOpenTelemetry().getMeterProvider())
+ .isEqualTo(MeterProvider.noop());
+ }
+
+ @Test
+ public void usesCustomOpenTelemetryFromOptions() {
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ SdkMeterProvider sdkMeterProvider =
+ SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+ OpenTelemetry openTelemetry =
+ OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setOpenTelemetry(openTelemetry).build())
+ .build();
+ EnabledMetricsUtil metricsUtil = new EnabledMetricsUtil(firestoreOptions);
+
+ BuiltinMetricsProvider customMetricsProvider = metricsUtil.getCustomMetricsProvider();
+ assertThat(customMetricsProvider).isNotNull();
+ assertThat(customMetricsProvider.getOpenTelemetry()).isNotNull();
+ assertThat(customMetricsProvider.getOpenTelemetry()).isEqualTo(openTelemetry);
+ }
+
+ @Test
+ public void usesGlobalOpenTelemetryIfCustomOpenTelemetryInstanceNotProvided() {
+ 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() {
+ InMemoryMetricReader inMemoryMetricReader = InMemoryMetricReader.create();
+ SdkMeterProvider sdkMeterProvider =
+ SdkMeterProvider.builder().registerMetricReader(inMemoryMetricReader).build();
+ OpenTelemetry openTelemetry =
+ OpenTelemetrySdk.builder().setMeterProvider(sdkMeterProvider).build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setOpenTelemetry(openTelemetry).build())
+ .build();
+ EnabledMetricsUtil metricsUtil = new EnabledMetricsUtil(firestoreOptions);
+
+ BuiltinMetricsProvider defaultMetricsProvider = metricsUtil.getDefaultMetricsProvider();
+ BuiltinMetricsProvider customMetricsProvider = metricsUtil.getCustomMetricsProvider();
+ assertThat(defaultMetricsProvider).isNotNull();
+ assertThat(customMetricsProvider).isNotNull();
+ assertThat(defaultMetricsProvider).isNotEqualTo(customMetricsProvider);
+ assertThat(defaultMetricsProvider.getOpenTelemetry())
+ .isNotEqualTo(customMetricsProvider.getOpenTelemetry());
+ }
+
+ @Test
+ public void canCreateMetricsContext() {
+ MetricsContext context = newEnabledMetricsUtil().createMetricsContext("testMethod");
+ assertThat(context).isNotNull();
+ assertThat(context instanceof EnabledMetricsUtil.MetricsContext).isTrue();
+ assertThat(context.methodName).isEqualTo("testMethod");
+ }
+
+ @Test
+ public void addsMetricsTracerFactoryForDefaultMetricsProvider() {
+ List factories = new ArrayList<>();
+
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ metricsUtil.addMetricsTracerFactory(factories);
+ // Adds tracer factory for default metrics provider only as the custom metrics provider is not
+ // enabled.
+ assertThat(factories.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void addsMetricsTracerFactoriesForBothMetricsProvider() {
+ List factories = new ArrayList<>();
+
+ OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setOpenTelemetry(openTelemetry).build())
+ .build();
+ EnabledMetricsUtil metricsUtil = new EnabledMetricsUtil(firestoreOptions);
+
+ metricsUtil.addMetricsTracerFactory(factories);
+ assertThat(factories.size()).isEqualTo(2);
+ }
+
+ @Test
+ public void addsMetricsTracerFactoriesIndependentlyForMetricsProviders() {
+ List factories = new ArrayList<>();
+
+ OpenTelemetry openTelemetry = OpenTelemetrySdk.builder().build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .setOpenTelemetry(openTelemetry)
+ .exportBuiltinMetricsToGoogleCloudMonitoring(false)
+ .build())
+ .build();
+ EnabledMetricsUtil metricsUtil = new EnabledMetricsUtil(firestoreOptions);
+
+ metricsUtil.addMetricsTracerFactory(factories);
+ // Add tracer factory for custom metrics provider only as the default metrics provider is not
+ // enabled.
+ assertThat(factories.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void extractsErrorStatusFromFirestoreException() {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ FirestoreException firestoreException =
+ FirestoreException.forApiException(
+ new ApiException(
+ new IllegalStateException("Mock batchWrite failed in test"),
+ GrpcStatusCode.of(Status.Code.INVALID_ARGUMENT),
+ false));
+
+ String errorStatus = metricsUtil.extractErrorStatus(firestoreException);
+ assertThat(errorStatus).isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString());
+ }
+
+ @Test
+ public void extractsErrorStatusFromApiException() {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ ApiException apiException =
+ new ApiException(
+ new IllegalStateException("Mock batchWrite failed in test"),
+ GrpcStatusCode.of(Status.Code.INVALID_ARGUMENT),
+ false);
+ String errorStatus = metricsUtil.extractErrorStatus(apiException);
+ assertThat(errorStatus).isEqualTo(StatusCode.Code.INVALID_ARGUMENT.toString());
+ }
+
+ @Test
+ public void errorStatusSetToUnknownOnNotRecognizedException() {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ Throwable t = new Throwable(new IllegalStateException("Mock batchWrite failed in test"));
+ String errorStatus = metricsUtil.extractErrorStatus(t);
+ assertThat(errorStatus).isEqualTo(StatusCode.Code.UNKNOWN.toString());
+ }
+
+ // Use reflection to inject mock metrics provider
+ private void injectMockProviders(EnabledMetricsUtil metricsUtil) throws Exception {
+ injectMockProvider(metricsUtil, "defaultMetricsProvider", defaultProvider);
+ injectMockProvider(metricsUtil, "customMetricsProvider", customProvider);
+ }
+
+ private void injectMockProvider(
+ EnabledMetricsUtil metricsUtil, String fieldName, BuiltinMetricsProvider provider)
+ throws Exception {
+ Field field = EnabledMetricsUtil.class.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ field.set(metricsUtil, provider);
+ }
+
+ @Test
+ public void recordLatencyCalledWhenFutureIsCompletedWithSuccess() throws Exception {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ injectMockProviders(metricsUtil);
+
+ MetricsContext context = metricsUtil.createMetricsContext("testMethod");
+ ApiFuture future = ApiFutures.immediateFuture("success");
+ context.recordLatencyAtFuture(MetricType.FIRST_RESPONSE_LATENCY, future);
+
+ verify(defaultProvider)
+ .latencyRecorder(eq(MetricType.FIRST_RESPONSE_LATENCY), anyDouble(), Mockito.anyMap());
+ verify(customProvider)
+ .latencyRecorder(eq(MetricType.FIRST_RESPONSE_LATENCY), anyDouble(), Mockito.anyMap());
+ }
+
+ @Test
+ public void recordLatencyCalledWhenFutureIsCompletedWithError() throws Exception {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ injectMockProviders(metricsUtil);
+
+ MetricsContext context = metricsUtil.createMetricsContext("testMethod");
+ FirestoreException firestoreException =
+ FirestoreException.forApiException(
+ new ApiException(
+ new IllegalStateException("Mock batchWrite failed in test"),
+ GrpcStatusCode.of(Status.Code.INVALID_ARGUMENT),
+ false));
+ ApiFuture future = ApiFutures.immediateFailedFuture(firestoreException);
+ context.recordLatencyAtFuture(MetricType.FIRST_RESPONSE_LATENCY, future);
+
+ // TODO(b/376473320):Change this to correct status code
+ Mockito.verify(defaultProvider)
+ .latencyRecorder(
+ Mockito.eq(MetricType.FIRST_RESPONSE_LATENCY),
+ Mockito.anyDouble(),
+ Mockito.argThat(
+ attributes ->
+ attributes
+ .get(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS)
+ .equals(Status.Code.INVALID_ARGUMENT.toString())));
+ Mockito.verify(customProvider)
+ .latencyRecorder(
+ Mockito.eq(MetricType.FIRST_RESPONSE_LATENCY),
+ Mockito.anyDouble(),
+ Mockito.argThat(
+ attributes ->
+ attributes
+ .get(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS)
+ .equals(Status.Code.INVALID_ARGUMENT.toString())));
+ }
+
+ @Test
+ public void recordCounterCalledWhenFutureIsCompletedWithSuccess() throws Exception {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ injectMockProviders(metricsUtil);
+
+ MetricsContext context = metricsUtil.createMetricsContext("testMethod");
+ ApiFuture future = ApiFutures.immediateFuture("success");
+ context.recordCounterAtFuture(MetricType.TRANSACTION_ATTEMPT_COUNT, future);
+
+ Mockito.verify(defaultProvider)
+ .counterRecorder(
+ Mockito.eq(MetricType.TRANSACTION_ATTEMPT_COUNT), Mockito.anyLong(), Mockito.anyMap());
+ Mockito.verify(customProvider)
+ .counterRecorder(
+ Mockito.eq(MetricType.TRANSACTION_ATTEMPT_COUNT), Mockito.anyLong(), Mockito.anyMap());
+ }
+
+ @Test
+ public void recordCounterCalledWhenFutureIsCompletedWithError() throws Exception {
+ EnabledMetricsUtil metricsUtil = newEnabledMetricsUtil();
+ injectMockProviders(metricsUtil);
+
+ MetricsContext context = metricsUtil.createMetricsContext("testMethod");
+ FirestoreException firestoreException =
+ FirestoreException.forApiException(
+ new ApiException(
+ new IllegalStateException("Mock batchWrite failed in test"),
+ GrpcStatusCode.of(Status.Code.INVALID_ARGUMENT),
+ false));
+ ApiFuture future = ApiFutures.immediateFailedFuture(firestoreException);
+ context.recordCounterAtFuture(MetricType.TRANSACTION_ATTEMPT_COUNT, future);
+
+ Mockito.verify(defaultProvider)
+ .counterRecorder(
+ Mockito.eq(MetricType.TRANSACTION_ATTEMPT_COUNT),
+ Mockito.anyLong(),
+ Mockito.argThat(
+ attributes ->
+ attributes
+ .get(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS)
+ .equals(Status.Code.INVALID_ARGUMENT.toString())));
+ Mockito.verify(customProvider)
+ .counterRecorder(
+ Mockito.eq(MetricType.TRANSACTION_ATTEMPT_COUNT),
+ Mockito.anyLong(),
+ Mockito.argThat(
+ attributes ->
+ attributes
+ .get(TelemetryConstants.METRIC_ATTRIBUTE_KEY_STATUS)
+ .equals(Status.Code.INVALID_ARGUMENT.toString())));
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/MetricsUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/MetricsUtilTest.java
new file mode 100644
index 000000000..8aa5d3645
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/MetricsUtilTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.telemetry;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.*;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.firestore.FirestoreOptions;
+import org.junit.Test;
+
+public class MetricsUtilTest {
+ @Test
+ public void defaultFirestoreOptionsCreatesEnabledMetricsUtil() {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof EnabledMetricsUtil).isTrue();
+ }
+
+ @Test
+ public void createEnabledMetricsUtilWithEnvOn() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", "ON")
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof EnabledMetricsUtil).isTrue();
+ });
+ }
+
+ @Test
+ public void createEnabledMetricsUtilWithEnvTrue() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", "True")
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof EnabledMetricsUtil).isTrue();
+ });
+ }
+
+ @Test
+ public void createDisabledMetricsUtilWithEnvOff() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", "OFF")
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof DisabledMetricsUtil).isTrue();
+ });
+ }
+
+ @Test
+ public void createDisabledMetricsUtilWithEnvFalse() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", "false")
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof DisabledMetricsUtil).isTrue();
+ });
+ }
+
+ @Test
+ public void invalidEnvironmentVariableDefaultsToEnabledMetricsUtil() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", "Invalid")
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+
+ assertThat(util instanceof EnabledMetricsUtil).isTrue();
+ });
+ }
+
+ @Test
+ public void nullEnvironmentVariableDefaultsToEnabledMetricsUtil() throws Exception {
+ withEnvironmentVariable("FIRESTORE_ENABLE_METRICS", null)
+ .execute(
+ () -> {
+ MetricsUtil util =
+ MetricsUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+ assertThat(util instanceof EnabledMetricsUtil).isTrue();
+ });
+ }
+}