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 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(); + }); + } +}