diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml
index 1cc16ad72..2cc42db69 100644
--- a/google-cloud-firestore/pom.xml
+++ b/google-cloud-firestore/pom.xml
@@ -16,6 +16,7 @@
google-cloud-firestore
+ 1.29.0
@@ -116,7 +117,17 @@
io.opentelemetry
opentelemetry-api
- 1.29.0
+ ${opentelemetry.version}
+
+
+ io.opentelemetry
+ opentelemetry-context
+ ${opentelemetry.version}
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-grpc-1.6
+ ${opentelemetry.version}-alpha
@@ -179,6 +190,14 @@
3.14.0
test
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+ ${opentelemetry.version}
+ test
+
+
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 b3d5bacf0..419602063 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
@@ -22,7 +22,6 @@
public class FirestoreOpenTelemetryOptions {
private final boolean enabled;
-
private final @Nullable OpenTelemetry openTelemetry;
FirestoreOpenTelemetryOptions(Builder builder) {
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java
index 3cb67e5ec..69a59c631 100644
--- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FirestoreOptions.java
@@ -16,6 +16,7 @@
package com.google.cloud.firestore;
+import com.google.api.core.ApiFunction;
import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import com.google.api.gax.core.CredentialsProvider;
@@ -62,6 +63,7 @@ public final class FirestoreOptions extends ServiceOptions channelConfigurator =
+ this.traceUtil.getChannelConfigurator();
+ if (channelConfigurator == null) {
+ this.channelProvider =
+ GrpcTransportOptions.setUpChannelProvider(
FirestoreSettings.defaultGrpcTransportProviderBuilder(), this);
+ } else {
+ // Intercept the grpc channel calls to add telemetry info.
+ this.channelProvider =
+ GrpcTransportOptions.setUpChannelProvider(
+ FirestoreSettings.defaultGrpcTransportProviderBuilder()
+ .setChannelConfigurator(channelConfigurator),
+ this);
+ }
+ } else {
+ this.channelProvider = builder.channelProvider;
+ }
this.credentialsProvider =
builder.credentialsProvider != null
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/DisabledTraceUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/DisabledTraceUtil.java
new file mode 100644
index 000000000..229df201f
--- /dev/null
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/DisabledTraceUtil.java
@@ -0,0 +1,103 @@
+/*
+ * 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 com.google.api.core.ApiFunction;
+import com.google.api.core.ApiFuture;
+import io.grpc.ManagedChannelBuilder;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public class DisabledTraceUtil implements TraceUtil {
+
+ static class Span implements TraceUtil.Span {
+ @Override
+ public void end() {}
+
+ @Override
+ public void end(Throwable error) {}
+
+ @Override
+ public void endAtFuture(ApiFuture futureValue) {}
+
+ @Override
+ public TraceUtil.Span addEvent(String name) {
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span addEvent(String name, Map attributes) {
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span setAttribute(String key, int value) {
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span setAttribute(String key, String value) {
+ return this;
+ }
+
+ @Override
+ public Scope makeCurrent() {
+ return new Scope();
+ }
+ }
+
+ static class Context implements TraceUtil.Context {
+ @Override
+ public Scope makeCurrent() {
+ return new Scope();
+ }
+ }
+
+ static class Scope implements TraceUtil.Scope {
+ @Override
+ public void close() {}
+ }
+
+ @Nullable
+ @Override
+ public ApiFunction getChannelConfigurator() {
+ return null;
+ }
+
+ @Override
+ public Span startSpan(String spanName) {
+ return new Span();
+ }
+
+ @Override
+ public TraceUtil.Span startSpan(String spanName, TraceUtil.Context parent) {
+ return new Span();
+ }
+
+ @Nonnull
+ @Override
+ public TraceUtil.Span currentSpan() {
+ return new Span();
+ }
+
+ @Nonnull
+ @Override
+ public TraceUtil.Context currentContext() {
+ return new DisabledTraceUtil.Context();
+ }
+}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledTraceUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledTraceUtil.java
new file mode 100644
index 000000000..dccd3cefe
--- /dev/null
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/EnabledTraceUtil.java
@@ -0,0 +1,332 @@
+/*
+ * 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 com.google.api.core.ApiFunction;
+import com.google.api.core.ApiFuture;
+import com.google.api.core.ApiFutureCallback;
+import com.google.api.core.ApiFutures;
+import com.google.cloud.firestore.FirestoreOptions;
+import com.google.common.base.Throwables;
+import io.grpc.ManagedChannelBuilder;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.common.AttributesBuilder;
+import io.opentelemetry.api.trace.SpanBuilder;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.instrumentation.grpc.v1_6.GrpcTelemetry;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public class EnabledTraceUtil implements TraceUtil {
+ private final Tracer tracer;
+ private final OpenTelemetry openTelemetry;
+ private final FirestoreOptions firestoreOptions;
+
+ EnabledTraceUtil(FirestoreOptions firestoreOptions) {
+ OpenTelemetry openTelemetry = firestoreOptions.getOpenTelemetryOptions().getOpenTelemetry();
+
+ // If tracing is enabled, but an OpenTelemetry instance is not provided, fall back
+ // to using GlobalOpenTelemetry.
+ if (openTelemetry == null) {
+ openTelemetry = GlobalOpenTelemetry.get();
+ }
+
+ this.firestoreOptions = firestoreOptions;
+ this.openTelemetry = openTelemetry;
+ this.tracer = openTelemetry.getTracer(LIBRARY_NAME);
+ }
+
+ public OpenTelemetry getOpenTelemetry() {
+ return openTelemetry;
+ }
+
+ // The gRPC channel configurator that intercepts gRPC calls for tracing purposes.
+ public class OpenTelemetryGrpcChannelConfigurator
+ implements ApiFunction {
+ @Override
+ public ManagedChannelBuilder apply(ManagedChannelBuilder managedChannelBuilder) {
+ GrpcTelemetry grpcTelemetry = GrpcTelemetry.create(getOpenTelemetry());
+ return managedChannelBuilder.intercept(grpcTelemetry.newClientInterceptor());
+ }
+ }
+
+ @Override
+ @Nullable
+ public ApiFunction getChannelConfigurator() {
+ return new OpenTelemetryGrpcChannelConfigurator();
+ }
+
+ static class Span implements TraceUtil.Span {
+ private final io.opentelemetry.api.trace.Span span;
+ private final String spanName;
+
+ public Span(io.opentelemetry.api.trace.Span span, String spanName) {
+ this.span = span;
+ this.spanName = spanName;
+ }
+
+ /** Ends this span. */
+ @Override
+ public void end() {
+ span.end();
+ }
+
+ /** Ends this span in an error. */
+ @Override
+ public void end(Throwable error) {
+ span.setStatus(StatusCode.ERROR, error.getMessage());
+ span.recordException(
+ error,
+ Attributes.builder()
+ .put("exception.message", error.getMessage())
+ .put("exception.type", error.getClass().getName())
+ .put("exception.stacktrace", Throwables.getStackTraceAsString(error))
+ .build());
+ span.end();
+ }
+
+ /**
+ * If an operation ends in the future, its relevant span should end _after_ the future has been
+ * completed. This method "appends" the span completion code at the completion of the given
+ * future. In order for telemetry info to be recorded, the future returned by this method should
+ * be completed.
+ */
+ @Override
+ public void endAtFuture(ApiFuture futureValue) {
+ io.opentelemetry.context.Context asyncContext = io.opentelemetry.context.Context.current();
+ ApiFutures.addCallback(
+ futureValue,
+ new ApiFutureCallback() {
+ @Override
+ public void onFailure(Throwable t) {
+ try (io.opentelemetry.context.Scope scope = asyncContext.makeCurrent()) {
+ span.addEvent(spanName + " failed.");
+ end(t);
+ }
+ }
+
+ @Override
+ public void onSuccess(T result) {
+ try (io.opentelemetry.context.Scope scope = asyncContext.makeCurrent()) {
+ span.addEvent(spanName + " succeeded.");
+ end();
+ }
+ }
+ });
+ }
+
+ /** Adds the given event to this span. */
+ @Override
+ public TraceUtil.Span addEvent(String name) {
+ span.addEvent(name);
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span addEvent(String name, Map attributes) {
+ AttributesBuilder attributesBuilder = Attributes.builder();
+ attributes.forEach(
+ (key, value) -> {
+ if (value instanceof Integer) {
+ attributesBuilder.put(key, (int) value);
+ } else if (value instanceof Long) {
+ attributesBuilder.put(key, (long) value);
+ } else if (value instanceof Double) {
+ attributesBuilder.put(key, (double) value);
+ } else if (value instanceof Float) {
+ attributesBuilder.put(key, (float) value);
+ } else if (value instanceof Boolean) {
+ attributesBuilder.put(key, (boolean) value);
+ } else if (value instanceof String) {
+ attributesBuilder.put(key, (String) value);
+ } else {
+ // OpenTelemetry APIs do not support any other type.
+ throw new IllegalArgumentException(
+ "Unknown attribute type:" + value.getClass().getSimpleName());
+ }
+ });
+ span.addEvent(name, attributesBuilder.build());
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span setAttribute(String key, int value) {
+ span.setAttribute(ATTRIBUTE_SERVICE_PREFIX + key, value);
+ return this;
+ }
+
+ @Override
+ public TraceUtil.Span setAttribute(String key, String value) {
+ span.setAttribute(ATTRIBUTE_SERVICE_PREFIX + key, value);
+ return this;
+ }
+
+ @Override
+ public Scope makeCurrent() {
+ return new Scope(span.makeCurrent());
+ }
+ }
+
+ static class Scope implements TraceUtil.Scope {
+ private final io.opentelemetry.context.Scope scope;
+
+ Scope(io.opentelemetry.context.Scope scope) {
+ this.scope = scope;
+ }
+
+ @Override
+ public void close() {
+ scope.close();
+ }
+ }
+
+ static class Context implements TraceUtil.Context {
+ private final io.opentelemetry.context.Context context;
+
+ Context(io.opentelemetry.context.Context context) {
+ this.context = context;
+ }
+
+ @Override
+ public Scope makeCurrent() {
+ return new Scope(context.makeCurrent());
+ }
+ }
+
+ /** Applies the current Firestore instance settings as attributes to the current Span */
+ private SpanBuilder addSettingsAttributesToCurrentSpan(SpanBuilder spanBuilder) {
+ spanBuilder =
+ spanBuilder.setAllAttributes(
+ Attributes.builder()
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.databaseId",
+ firestoreOptions.getDatabaseId())
+ .put(ATTRIBUTE_SERVICE_PREFIX + "settings.host", firestoreOptions.getHost())
+ .build());
+
+ if (firestoreOptions.getTransportChannelProvider() != null) {
+ spanBuilder =
+ spanBuilder.setAllAttributes(
+ Attributes.builder()
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.channel.transportName",
+ firestoreOptions.getTransportChannelProvider().getTransportName())
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.channel.needsCredentials",
+ String.valueOf(
+ firestoreOptions.getTransportChannelProvider().needsCredentials()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.channel.needsEndpoint",
+ String.valueOf(
+ firestoreOptions.getTransportChannelProvider().needsEndpoint()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.channel.needsHeaders",
+ String.valueOf(firestoreOptions.getTransportChannelProvider().needsHeaders()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.channel.shouldAutoClose",
+ String.valueOf(
+ firestoreOptions.getTransportChannelProvider().shouldAutoClose()))
+ .build());
+ }
+
+ if (firestoreOptions.getCredentials() != null) {
+ spanBuilder =
+ spanBuilder.setAttribute(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.credentials.authenticationType",
+ firestoreOptions.getCredentials().getAuthenticationType());
+ }
+
+ if (firestoreOptions.getRetrySettings() != null) {
+ spanBuilder =
+ spanBuilder.setAllAttributes(
+ Attributes.builder()
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.initialRetryDelay",
+ firestoreOptions.getRetrySettings().getInitialRetryDelay().toString())
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.maxRetryDelay",
+ firestoreOptions.getRetrySettings().getMaxRetryDelay().toString())
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.retryDelayMultiplier",
+ String.valueOf(firestoreOptions.getRetrySettings().getRetryDelayMultiplier()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.maxAttempts",
+ String.valueOf(firestoreOptions.getRetrySettings().getMaxAttempts()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.initialRpcTimeout",
+ firestoreOptions.getRetrySettings().getInitialRpcTimeout().toString())
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.maxRpcTimeout",
+ firestoreOptions.getRetrySettings().getMaxRpcTimeout().toString())
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.rpcTimeoutMultiplier",
+ String.valueOf(firestoreOptions.getRetrySettings().getRpcTimeoutMultiplier()))
+ .put(
+ ATTRIBUTE_SERVICE_PREFIX + "settings.retrySettings.totalTimeout",
+ firestoreOptions.getRetrySettings().getTotalTimeout().toString())
+ .build());
+ }
+
+ // Add the memory utilization of the client at the time this trace was collected.
+ long totalMemory = Runtime.getRuntime().totalMemory();
+ long freeMemory = Runtime.getRuntime().freeMemory();
+ double memoryUtilization = ((double) (totalMemory - freeMemory)) / totalMemory;
+ spanBuilder.setAttribute(
+ ATTRIBUTE_SERVICE_PREFIX + "memoryUtilization",
+ String.format("%.2f", memoryUtilization * 100) + "%");
+
+ return spanBuilder;
+ }
+
+ @Override
+ public Span startSpan(String spanName) {
+ SpanBuilder spanBuilder = tracer.spanBuilder(spanName).setSpanKind(SpanKind.PRODUCER);
+ io.opentelemetry.api.trace.Span span =
+ addSettingsAttributesToCurrentSpan(spanBuilder).startSpan();
+ return new Span(span, spanName);
+ }
+
+ @Override
+ public TraceUtil.Span startSpan(String spanName, TraceUtil.Context parent) {
+ assert (parent instanceof EnabledTraceUtil.Context);
+ SpanBuilder spanBuilder =
+ tracer
+ .spanBuilder(spanName)
+ .setSpanKind(SpanKind.PRODUCER)
+ .setParent(((EnabledTraceUtil.Context) parent).context);
+ io.opentelemetry.api.trace.Span span =
+ addSettingsAttributesToCurrentSpan(spanBuilder).startSpan();
+ return new Span(span, spanName);
+ }
+
+ @Nonnull
+ @Override
+ public TraceUtil.Span currentSpan() {
+ return new Span(io.opentelemetry.api.trace.Span.current(), "");
+ }
+
+ @Nonnull
+ @Override
+ public TraceUtil.Context currentContext() {
+ return new Context(io.opentelemetry.context.Context.current());
+ }
+}
diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TraceUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TraceUtil.java
new file mode 100644
index 000000000..b7241ad97
--- /dev/null
+++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/telemetry/TraceUtil.java
@@ -0,0 +1,151 @@
+/*
+ * 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 com.google.api.core.ApiFunction;
+import com.google.api.core.ApiFuture;
+import com.google.cloud.firestore.FirestoreOptions;
+import io.grpc.ManagedChannelBuilder;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public interface TraceUtil {
+ String ATTRIBUTE_SERVICE_PREFIX = "gcp.firestore.";
+ String SPAN_NAME_DOC_REF_CREATE = "DocumentReference.Create";
+ String SPAN_NAME_DOC_REF_SET = "DocumentReference.Set";
+ String SPAN_NAME_DOC_REF_UPDATE = "DocumentReference.Update";
+ String SPAN_NAME_DOC_REF_DELETE = "DocumentReference.Delete";
+ String SPAN_NAME_DOC_REF_GET = "DocumentReference.Get";
+ String SPAN_NAME_DOC_REF_LIST_COLLECTIONS = "DocumentReference.ListCollections";
+ String SPAN_NAME_COL_REF_ADD = "CollectionReference.Add";
+ String SPAN_NAME_COL_REF_LIST_DOCUMENTS = "CollectionReference.ListDocuments";
+ String SPAN_NAME_QUERY_GET = "Query.Get";
+ String SPAN_NAME_AGGREGATION_QUERY_GET = "AggregationQuery.Get";
+ String SPAN_NAME_RUN_QUERY = "RunQuery";
+ String SPAN_NAME_RUN_AGGREGATION_QUERY = "RunAggregationQuery";
+ String SPAN_NAME_BATCH_GET_DOCUMENTS = "BatchGetDocuments";
+ String SPAN_NAME_TRANSACTION_RUN = "Transaction.Run";
+ String SPAN_NAME_TRANSACTION_BEGIN = "Transaction.Begin";
+ String SPAN_NAME_TRANSACTION_GET_QUERY = "Transaction.Get.Query";
+ String SPAN_NAME_TRANSACTION_GET_AGGREGATION_QUERY = "Transaction.Get.AggregationQuery";
+ String SPAN_NAME_TRANSACTION_GET_DOCUMENT = "Transaction.Get.Document";
+ String SPAN_NAME_TRANSACTION_GET_DOCUMENTS = "Transaction.Get.Documents";
+ String SPAN_NAME_TRANSACTION_ROLLBACK = "Transaction.Rollback";
+ String SPAN_NAME_BATCH_COMMIT = "Batch.Commit";
+ String SPAN_NAME_TRANSACTION_COMMIT = "Transaction.Commit";
+ String SPAN_NAME_PARTITION_QUERY = "PartitionQuery";
+ String SPAN_NAME_BULK_WRITER_COMMIT = "BulkWriter.Commit";
+
+ String ENABLE_TRACING_ENV_VAR = "FIRESTORE_ENABLE_TRACING";
+ String LIBRARY_NAME = "com.google.cloud.firestore";
+
+ /**
+ * Creates and returns an instance of the TraceUtil class.
+ *
+ * @param firestoreOptions The FirestoreOptions object that is requesting an instance of
+ * TraceUtil.
+ * @return An instance of the TraceUtil class.
+ */
+ static TraceUtil getInstance(@Nonnull FirestoreOptions firestoreOptions) {
+ boolean createEnabledInstance = firestoreOptions.getOpenTelemetryOptions().getEnabled();
+
+ // The environment variable can override options to enable/disable telemetry collection.
+ String enableTracingEnvVar = System.getenv(ENABLE_TRACING_ENV_VAR);
+ if (enableTracingEnvVar != null) {
+ if (enableTracingEnvVar.equalsIgnoreCase("true")
+ || enableTracingEnvVar.equalsIgnoreCase("on")) {
+ createEnabledInstance = true;
+ }
+ if (enableTracingEnvVar.equalsIgnoreCase("false")
+ || enableTracingEnvVar.equalsIgnoreCase("off")) {
+ createEnabledInstance = false;
+ }
+ }
+
+ if (createEnabledInstance) {
+ return new EnabledTraceUtil(firestoreOptions);
+ } else {
+ return new DisabledTraceUtil();
+ }
+ }
+
+ /** Returns a channel configurator for gRPC, or {@code null} if tracing is disabled. */
+ @Nullable
+ ApiFunction getChannelConfigurator();
+
+ /** Represents a trace span. */
+ interface Span {
+ /** Adds the given event to this span. */
+ Span addEvent(String name);
+
+ /** Adds the given event with the given attributes to this span. */
+ Span addEvent(String name, Map attributes);
+
+ /** Adds the given attribute to this span. */
+ Span setAttribute(String key, int value);
+
+ /** Adds the given attribute to this span. */
+ Span setAttribute(String key, String value);
+
+ /** Marks this span as the current span. */
+ Scope makeCurrent();
+
+ /** Ends this span. */
+ void end();
+
+ /** Ends this span in an error. */
+ void end(Throwable error);
+
+ /**
+ * If an operation ends in the future, its relevant span should end _after_ the future has been
+ * completed. This method "appends" the span completion code at the completion of the given
+ * future. In order for telemetry info to be recorded, the future returned by this method should
+ * be completed.
+ */
+ void endAtFuture(ApiFuture futureValue);
+ }
+
+ /** Represents a trace context. */
+ interface Context {
+ /** Makes this context the current context. */
+ Scope makeCurrent();
+ }
+
+ /** Represents a trace scope. */
+ interface Scope extends AutoCloseable {
+ /** Closes the current scope. */
+ void close();
+ }
+
+ /** Starts a new span with the given name, sets it as the current span, and returns it. */
+ Span startSpan(String spanName);
+
+ /**
+ * Starts a new span with the given name and the given context as its parent, sets it as the
+ * current span, and returns it.
+ */
+ Span startSpan(String spanName, Context parent);
+
+ /** Returns the current span. */
+ @Nonnull
+ Span currentSpan();
+
+ /** Returns the current Context. */
+ @Nonnull
+ Context currentContext();
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/OpenTelemetryOptionsTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/OpenTelemetryOptionsTest.java
index a8074cd2d..c75b0feb7 100644
--- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/OpenTelemetryOptionsTest.java
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/OpenTelemetryOptionsTest.java
@@ -18,8 +18,11 @@
import static com.google.common.truth.Truth.assertThat;
+import com.google.cloud.firestore.telemetry.DisabledTraceUtil;
+import com.google.cloud.firestore.telemetry.EnabledTraceUtil;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
import javax.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
@@ -51,6 +54,8 @@ public void defaultOptionsDisablesTelemetryCollection() {
firestore = firestoreOptions.getService();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getEnabled()).isFalse();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getOpenTelemetry()).isNull();
+ assertThat(firestore.getOptions().getTraceUtil()).isNotNull();
+ assertThat(firestore.getOptions().getTraceUtil() instanceof DisabledTraceUtil).isTrue();
}
@Test
@@ -63,6 +68,8 @@ public void canEnableTelemetryCollectionWithoutOpenTelemetryInstance() {
firestore = firestoreOptions.getService();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getEnabled()).isTrue();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getOpenTelemetry()).isNull();
+ assertThat(firestore.getOptions().getTraceUtil()).isNotNull();
+ assertThat(firestore.getOptions().getTraceUtil() instanceof EnabledTraceUtil).isTrue();
}
@Test
@@ -80,6 +87,8 @@ public void canEnableTelemetryCollectionWithOpenTelemetryInstance() {
assertThat(firestore.getOptions().getOpenTelemetryOptions().getEnabled()).isTrue();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getOpenTelemetry())
.isEqualTo(openTelemetry);
+ assertThat(firestore.getOptions().getTraceUtil()).isNotNull();
+ assertThat(firestore.getOptions().getTraceUtil() instanceof EnabledTraceUtil).isTrue();
}
@Test
@@ -97,5 +106,55 @@ public void canDisableTelemetryCollectionWhileOpenTelemetryInstanceIsNotNull() {
assertThat(firestore.getOptions().getOpenTelemetryOptions().getEnabled()).isFalse();
assertThat(firestore.getOptions().getOpenTelemetryOptions().getOpenTelemetry())
.isEqualTo(openTelemetry);
+ assertThat(firestore.getOptions().getTraceUtil()).isNotNull();
+ assertThat(firestore.getOptions().getTraceUtil() instanceof DisabledTraceUtil).isTrue();
+ }
+
+ @Test
+ public void existenceOfGlobalOpenTelemetryDoesNotEnableTracing() {
+ // Register a global OpenTelemetry SDK.
+ OpenTelemetrySdk.builder().buildAndRegisterGlobal();
+
+ // Make sure Firestore does not use GlobalOpenTelemetry by default.
+ FirestoreOptions firestoreOptions = getBaseOptions().build();
+ firestore = firestoreOptions.getService();
+ assertThat(firestore.getOptions().getOpenTelemetryOptions().getEnabled()).isFalse();
+ assertThat(firestore.getOptions().getOpenTelemetryOptions().getOpenTelemetry()).isNull();
+ assertThat(firestore.getOptions().getTraceUtil()).isNotNull();
+ assertThat(firestore.getOptions().getTraceUtil() instanceof DisabledTraceUtil).isTrue();
+ }
+
+ @Test
+ public void canPassOpenTelemetrySdkInstanceToFirestore() {
+ OpenTelemetrySdk myOpenTelemetrySdk = OpenTelemetrySdk.builder().build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .setTracingEnabled(true)
+ .setOpenTelemetry(myOpenTelemetrySdk)
+ .build())
+ .build();
+ firestore = firestoreOptions.getService();
+ EnabledTraceUtil enabledTraceUtil = (EnabledTraceUtil) firestore.getOptions().getTraceUtil();
+ assertThat(enabledTraceUtil).isNotNull();
+ assertThat(enabledTraceUtil.getOpenTelemetry()).isEqualTo(myOpenTelemetrySdk);
+ }
+
+ @Test
+ public void usesGlobalOpenTelemetryIfOpenTelemetryNotProvidedInOptions() {
+ // Register a global OpenTelemetry SDK.
+ OpenTelemetrySdk.builder().buildAndRegisterGlobal();
+
+ // Do _not_ pass it to FirestoreOptions.
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setTracingEnabled(true).build())
+ .build();
+ firestore = firestoreOptions.getService();
+ EnabledTraceUtil enabledTraceUtil = (EnabledTraceUtil) firestore.getOptions().getTraceUtil();
+ assertThat(enabledTraceUtil).isNotNull();
+ assertThat(enabledTraceUtil.getOpenTelemetry()).isEqualTo(GlobalOpenTelemetry.get());
}
}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledTraceUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledTraceUtilTest.java
new file mode 100644
index 000000000..4e60fc748
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/DisabledTraceUtilTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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 org.junit.Test;
+
+public class DisabledTraceUtilTest {
+ @Test
+ public void disabledTraceUtilDoesNotProvideChannelConfigurator() {
+ DisabledTraceUtil traceUtil = new DisabledTraceUtil();
+ assertThat(traceUtil.getChannelConfigurator()).isNull();
+ }
+
+ @Test
+ public void usesDisabledContext() {
+ DisabledTraceUtil traceUtil = new DisabledTraceUtil();
+ assertThat(traceUtil.currentContext() instanceof DisabledTraceUtil.Context).isTrue();
+ }
+
+ @Test
+ public void usesDisabledSpan() {
+ DisabledTraceUtil traceUtil = new DisabledTraceUtil();
+ assertThat(traceUtil.currentSpan() instanceof DisabledTraceUtil.Span).isTrue();
+ assertThat(traceUtil.startSpan("foo") instanceof DisabledTraceUtil.Span).isTrue();
+ assertThat(
+ traceUtil.startSpan("foo", traceUtil.currentContext())
+ instanceof DisabledTraceUtil.Span)
+ .isTrue();
+ }
+
+ @Test
+ public void usesDisabledScope() {
+ DisabledTraceUtil traceUtil = new DisabledTraceUtil();
+ assertThat(traceUtil.currentContext().makeCurrent() instanceof DisabledTraceUtil.Scope)
+ .isTrue();
+ assertThat(traceUtil.currentSpan().makeCurrent() instanceof DisabledTraceUtil.Scope).isTrue();
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledTraceUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledTraceUtilTest.java
new file mode 100644
index 000000000..cfcdc5200
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/EnabledTraceUtilTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.cloud.firestore.FirestoreOpenTelemetryOptions;
+import com.google.cloud.firestore.FirestoreOptions;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import org.junit.Before;
+import org.junit.Test;
+
+public class EnabledTraceUtilTest {
+ @Before
+ public void setUp() {
+ GlobalOpenTelemetry.resetForTest();
+ }
+
+ FirestoreOptions.Builder getBaseOptions() {
+ return FirestoreOptions.newBuilder().setProjectId("test-project").setDatabaseId("(default)");
+ }
+
+ FirestoreOptions getTracingEnabledOptions() {
+ return getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setTracingEnabled(true).build())
+ .build();
+ }
+
+ EnabledTraceUtil newEnabledTraceUtil() {
+ return new EnabledTraceUtil(getTracingEnabledOptions());
+ }
+
+ @Test
+ public void usesOpenTelemetryFromOptions() {
+ OpenTelemetrySdk myOpenTelemetrySdk = OpenTelemetrySdk.builder().build();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder()
+ .setTracingEnabled(true)
+ .setOpenTelemetry(myOpenTelemetrySdk)
+ .build())
+ .build();
+ EnabledTraceUtil traceUtil = new EnabledTraceUtil(firestoreOptions);
+ assertThat(traceUtil.getOpenTelemetry()).isEqualTo(myOpenTelemetrySdk);
+ }
+
+ @Test
+ public void usesGlobalOpenTelemetryIfOpenTelemetryInstanceNotProvided() {
+ OpenTelemetrySdk.builder().buildAndRegisterGlobal();
+ FirestoreOptions firestoreOptions =
+ getBaseOptions()
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setTracingEnabled(true).build())
+ .build();
+ EnabledTraceUtil traceUtil = new EnabledTraceUtil(firestoreOptions);
+ assertThat(traceUtil.getOpenTelemetry()).isEqualTo(GlobalOpenTelemetry.get());
+ }
+
+ @Test
+ public void enabledTraceUtilProvidesChannelConfigurator() {
+ assertThat(newEnabledTraceUtil().getChannelConfigurator()).isNotNull();
+ }
+
+ @Test
+ public void usesEnabledContext() {
+ assertThat(newEnabledTraceUtil().currentContext() instanceof EnabledTraceUtil.Context).isTrue();
+ }
+
+ @Test
+ public void usesEnabledSpan() {
+ EnabledTraceUtil traceUtil = newEnabledTraceUtil();
+ assertThat(traceUtil.currentSpan() instanceof EnabledTraceUtil.Span).isTrue();
+ assertThat(traceUtil.startSpan("foo") instanceof EnabledTraceUtil.Span).isTrue();
+ assertThat(
+ traceUtil.startSpan("foo", traceUtil.currentContext()) instanceof EnabledTraceUtil.Span)
+ .isTrue();
+ }
+
+ @Test
+ public void usesEnabledScope() {
+ EnabledTraceUtil traceUtil = newEnabledTraceUtil();
+ assertThat(traceUtil.currentContext().makeCurrent() instanceof EnabledTraceUtil.Scope).isTrue();
+ assertThat(traceUtil.currentSpan().makeCurrent() instanceof EnabledTraceUtil.Scope).isTrue();
+ }
+}
diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/TraceUtilTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/TraceUtilTest.java
new file mode 100644
index 000000000..5bb3be668
--- /dev/null
+++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/telemetry/TraceUtilTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.cloud.firestore.FirestoreOpenTelemetryOptions;
+import com.google.cloud.firestore.FirestoreOptions;
+import org.junit.Test;
+
+public class TraceUtilTest {
+ @Test
+ public void defaultOptionsUseDisabledTraceUtil() {
+ TraceUtil traceUtil =
+ TraceUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .build());
+ assertThat(traceUtil instanceof DisabledTraceUtil).isTrue();
+ }
+
+ @Test
+ public void tracingDisabledOptionsUseDisabledTraceUtil() {
+ TraceUtil traceUtil =
+ TraceUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setTracingEnabled(false).build())
+ .build());
+ assertThat(traceUtil instanceof DisabledTraceUtil).isTrue();
+ }
+
+ @Test
+ public void tracingEnabledOptionsUseEnabledTraceUtil() {
+ TraceUtil traceUtil =
+ TraceUtil.getInstance(
+ FirestoreOptions.newBuilder()
+ .setProjectId("test-project")
+ .setDatabaseId("(default)")
+ .setOpenTelemetryOptions(
+ FirestoreOpenTelemetryOptions.newBuilder().setTracingEnabled(true).build())
+ .build());
+ assertThat(traceUtil instanceof EnabledTraceUtil).isTrue();
+ }
+}