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