fallbackProbingFunction) {
+ this.fallbackProbingFunction = fallbackProbingFunction;
+ return this;
+ }
+
+ public Builder setPrimaryProbingInterval(Duration primaryProbingInterval) {
+ this.primaryProbingInterval = primaryProbingInterval;
+ return this;
+ }
+
+ public Builder setFallbackProbingInterval(Duration fallbackProbingInterval) {
+ this.fallbackProbingInterval = fallbackProbingInterval;
+ return this;
+ }
+
+ public Builder setPrimaryChannelName(String primaryChannelName) {
+ this.primaryChannelName = primaryChannelName;
+ return this;
+ }
+
+ public Builder setFallbackChannelName(String fallbackChannelName) {
+ this.fallbackChannelName = fallbackChannelName;
+ return this;
+ }
+
+ public Builder setGcpFallbackOpenTelemetry(GcpFallbackOpenTelemetry openTelemetry) {
+ this.openTelemetry = openTelemetry;
+ return this;
+ }
+
+ public GcpFallbackChannelOptions build() {
+ return new GcpFallbackChannelOptions(this);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackOpenTelemetry.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackOpenTelemetry.java
new file mode 100644
index 0000000000..beaadb30bf
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackOpenTelemetry.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.bigtable.gaxx.grpc.fallback;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.cloud.bigtable.Version;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.metrics.Meter;
+import io.opentelemetry.api.metrics.MeterProvider;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * The entrypoint for OpenTelemetry metrics functionality in gRPC-GCP Fallback channel.
+ *
+ * GcpFallbackOpenTelemetry uses {@link io.opentelemetry.api.OpenTelemetry} APIs for
+ * instrumentation. When no SDK is explicitly added no telemetry data will be collected. See {@code
+ * io.opentelemetry.sdk.OpenTelemetrySdk} for information on how to construct the SDK.
+ */
+public final class GcpFallbackOpenTelemetry {
+ // TODO: confirm.
+ static final String INSTRUMENTATION_SCOPE = "cloudbigtable";
+ static final String METRIC_PREFIX = "eef";
+
+ static final String CURRENT_CHANNEL_METRIC = "current_channel";
+ static final String FALLBACK_COUNT_METRIC = "fallback_count";
+ static final String CALL_STATUS_METRIC = "call_status";
+ static final String ERROR_RATIO_METRIC = "error_ratio";
+ static final String PROBE_RESULT_METRIC = "probe_result";
+ static final String CHANNEL_DOWNTIME_METRIC = "channel_downtime";
+
+ static final AttributeKey CHANNEL_NAME = AttributeKey.stringKey("channel_name");
+ static final AttributeKey FROM_CHANNEL_NAME = AttributeKey.stringKey("from_channel_name");
+ static final AttributeKey TO_CHANNEL_NAME = AttributeKey.stringKey("to_channel_name");
+ static final AttributeKey STATUS_CODE = AttributeKey.stringKey("status_code");
+ static final AttributeKey PROBE_RESULT = AttributeKey.stringKey("result");
+
+ static final ImmutableSet DEFAULT_METRICS_SET =
+ ImmutableSet.of(
+ CURRENT_CHANNEL_METRIC,
+ FALLBACK_COUNT_METRIC,
+ CALL_STATUS_METRIC,
+ ERROR_RATIO_METRIC,
+ PROBE_RESULT_METRIC,
+ CHANNEL_DOWNTIME_METRIC);
+
+ private final OpenTelemetry openTelemetrySdk;
+ private final MeterProvider meterProvider;
+ private final Meter meter;
+ private final Map enableMetrics;
+ private final boolean disableDefault;
+ private final OpenTelemetryMetricsModule openTelemetryMetricsModule;
+
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ private GcpFallbackOpenTelemetry(Builder builder) {
+ this.openTelemetrySdk = checkNotNull(builder.openTelemetrySdk, "openTelemetrySdk");
+ this.meterProvider = checkNotNull(openTelemetrySdk.getMeterProvider(), "meterProvider");
+ this.meter =
+ this.meterProvider
+ .meterBuilder(INSTRUMENTATION_SCOPE)
+ .setInstrumentationVersion(Version.VERSION)
+ .build();
+ this.enableMetrics = ImmutableMap.copyOf(builder.enableMetrics);
+ this.disableDefault = builder.disableAll;
+ this.openTelemetryMetricsModule =
+ new OpenTelemetryMetricsModule(meter, enableMetrics, disableDefault);
+ }
+
+ /** Builder for configuring {@link GcpFallbackOpenTelemetry}. */
+ public static class Builder {
+ private OpenTelemetry openTelemetrySdk = OpenTelemetry.noop();
+ private final Map enableMetrics = new HashMap<>();
+ private boolean disableAll;
+
+ private Builder() {}
+
+ /**
+ * Sets the {@link io.opentelemetry.api.OpenTelemetry} entrypoint to use. This can be used to
+ * configure OpenTelemetry by returning the instance created by a {@code
+ * io.opentelemetry.sdk.OpenTelemetrySdkBuilder}.
+ */
+ public Builder withSdk(OpenTelemetry sdk) {
+ this.openTelemetrySdk = sdk;
+ return this;
+ }
+
+ /**
+ * Enables the specified metrics for collection and export. By default, all metrics are enabled.
+ */
+ public Builder enableMetrics(Collection enableMetrics) {
+ for (String metric : enableMetrics) {
+ this.enableMetrics.put(metric, true);
+ }
+ return this;
+ }
+
+ /** Disables the specified metrics from being collected and exported. */
+ public Builder disableMetrics(Collection disableMetrics) {
+ for (String metric : disableMetrics) {
+ this.enableMetrics.put(metric, false);
+ }
+ return this;
+ }
+
+ /** Disable all metrics. Any desired metric must be explicitly enabled after this. */
+ public Builder disableAllMetrics() {
+ this.enableMetrics.clear();
+ this.disableAll = true;
+ return this;
+ }
+
+ /**
+ * Returns a new {@link GcpFallbackOpenTelemetry} built with the configuration of this {@link
+ * Builder}.
+ */
+ public GcpFallbackOpenTelemetry build() {
+ return new GcpFallbackOpenTelemetry(this);
+ }
+ }
+
+ OpenTelemetryMetricsModule getModule() {
+ return openTelemetryMetricsModule;
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/MonitoringInterceptor.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/MonitoringInterceptor.java
new file mode 100644
index 0000000000..75b3ad6535
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/MonitoringInterceptor.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.bigtable.gaxx.grpc.fallback;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ClientInterceptor;
+import io.grpc.ForwardingClientCall;
+import io.grpc.ForwardingClientCallListener;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import io.grpc.Status.Code;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+
+class MonitoringInterceptor implements ClientInterceptor {
+ private Consumer statusCodeConsumer;
+
+ MonitoringInterceptor(Consumer statusCodeConsumer) {
+ this.statusCodeConsumer = statusCodeConsumer;
+ }
+
+ @Override
+ public ClientCall interceptCall(
+ MethodDescriptor method, CallOptions callOptions, Channel next) {
+ return new MonitoredClientCall<>(statusCodeConsumer, next, method, callOptions);
+ }
+
+ static class MonitoredClientCall extends ForwardingClientCall {
+
+ private final ClientCall delegateCall;
+ private final AtomicBoolean decremented = new AtomicBoolean(false);
+ private final Consumer statusCodeConsumer;
+
+ protected MonitoredClientCall(
+ Consumer statusCodeConsumer,
+ Channel channel,
+ MethodDescriptor methodDescriptor,
+ CallOptions callOptions) {
+ this.statusCodeConsumer = statusCodeConsumer;
+ this.delegateCall = channel.newCall(methodDescriptor, callOptions);
+ }
+
+ @Override
+ protected ClientCall delegate() {
+ return delegateCall;
+ }
+
+ @Override
+ public void start(Listener responseListener, Metadata headers) {
+
+ Listener listener =
+ new ForwardingClientCallListener.SimpleForwardingClientCallListener(
+ responseListener) {
+ @Override
+ public void onClose(Status status, Metadata trailers) {
+ // Use atomic to account for the race between onClose and cancel.
+ if (!decremented.getAndSet(true)) {
+ statusCodeConsumer.accept(status.getCode());
+ }
+ super.onClose(status, trailers);
+ }
+ };
+
+ delegateCall.start(listener, headers);
+ }
+
+ @Override
+ public void cancel(@Nullable String message, @Nullable Throwable cause) {
+ // Use atomic to account for the race between onClose and cancel.
+ if (!decremented.getAndSet(true)) {
+ statusCodeConsumer.accept(Status.Code.CANCELLED);
+ }
+ delegateCall.cancel(message, cause);
+ }
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsModule.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsModule.java
new file mode 100644
index 0000000000..89acff03a6
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsModule.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.bigtable.gaxx.grpc.fallback;
+
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CALL_STATUS_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CHANNEL_DOWNTIME_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CHANNEL_NAME;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CURRENT_CHANNEL_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.DEFAULT_METRICS_SET;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.ERROR_RATIO_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.FALLBACK_COUNT_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.FROM_CHANNEL_NAME;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.METRIC_PREFIX;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.PROBE_RESULT;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.PROBE_RESULT_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.STATUS_CODE;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.TO_CHANNEL_NAME;
+
+import io.grpc.Status.Code;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.Meter;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+final class OpenTelemetryMetricsModule {
+ private final OpenTelemetryMetricsResource resource;
+ private final Map firstFailure = new ConcurrentHashMap<>();
+ private final Map channelActive = new ConcurrentHashMap<>();
+
+ OpenTelemetryMetricsModule(
+ Meter meter, Map enableMetrics, boolean disableDefault) {
+ this.resource = createMetricInstruments(meter, enableMetrics, disableDefault);
+ }
+
+ static boolean isMetricEnabled(
+ String metricName, Map enableMetrics, boolean disableDefault) {
+ Boolean explicitlyEnabled = enableMetrics.get(metricName);
+ if (explicitlyEnabled != null) {
+ return explicitlyEnabled;
+ }
+ return DEFAULT_METRICS_SET.contains(metricName) && !disableDefault;
+ }
+
+ private OpenTelemetryMetricsResource createMetricInstruments(
+ Meter meter, Map enableMetrics, boolean disableDefault) {
+ OpenTelemetryMetricsResource.Builder builder = OpenTelemetryMetricsResource.builder();
+
+ if (isMetricEnabled(CURRENT_CHANNEL_METRIC, enableMetrics, disableDefault)) {
+ builder.currentChannelCounter(
+ meter
+ .upDownCounterBuilder(String.format("%s.%s", METRIC_PREFIX, CURRENT_CHANNEL_METRIC))
+ .setUnit("{channel}")
+ .setDescription("1 for currently active channel, 0 otherwise.")
+ .buildWithCallback(
+ counter -> {
+ channelActive.forEach(
+ (channelName, isActive) -> {
+ counter.record(
+ isActive ? 1 : 0, Attributes.of(CHANNEL_NAME, channelName));
+ });
+ }));
+ }
+
+ if (isMetricEnabled(FALLBACK_COUNT_METRIC, enableMetrics, disableDefault)) {
+ builder.fallbackCounter(
+ meter
+ .counterBuilder(String.format("%s.%s", METRIC_PREFIX, FALLBACK_COUNT_METRIC))
+ .setUnit("{occurrence}")
+ .setDescription("Number of fallbacks occurred from one channel to another.")
+ .build());
+ }
+
+ if (isMetricEnabled(CALL_STATUS_METRIC, enableMetrics, disableDefault)) {
+ builder.callStatusCounter(
+ meter
+ .counterBuilder(String.format("%s.%s", METRIC_PREFIX, CALL_STATUS_METRIC))
+ .setUnit("{call}")
+ .setDescription("Number of calls with a status and channel.")
+ .build());
+ }
+
+ if (isMetricEnabled(ERROR_RATIO_METRIC, enableMetrics, disableDefault)) {
+ builder.errorRatioGauge(
+ meter
+ .gaugeBuilder(String.format("%s.%s", METRIC_PREFIX, ERROR_RATIO_METRIC))
+ .setUnit("1")
+ .setDescription("Ratio of failed calls to total calls for a channel.")
+ .build());
+ }
+
+ if (isMetricEnabled(PROBE_RESULT_METRIC, enableMetrics, disableDefault)) {
+ builder.probeResultCounter(
+ meter
+ .counterBuilder(String.format("%s.%s", METRIC_PREFIX, PROBE_RESULT_METRIC))
+ .setUnit("{result}")
+ .setDescription("Results of probing functions execution.")
+ .build());
+ }
+
+ if (isMetricEnabled(CHANNEL_DOWNTIME_METRIC, enableMetrics, disableDefault)) {
+ builder.channelDowntimeGauge(
+ meter
+ .gaugeBuilder(String.format("%s.%s", METRIC_PREFIX, CHANNEL_DOWNTIME_METRIC))
+ .setUnit("s")
+ .setDescription("How many consecutive seconds probing fails for the channel.")
+ .build());
+ }
+
+ return builder.build();
+ }
+
+ void reportErrorRate(String channelName, float errorRate) {
+ DoubleGauge errorRatioGauge = resource.errorRatioGauge();
+
+ if (errorRatioGauge == null) {
+ return;
+ }
+
+ Attributes attributes = Attributes.of(CHANNEL_NAME, channelName);
+ errorRatioGauge.set(errorRate, attributes);
+ }
+
+ void reportStatus(String channelName, Code statusCode) {
+ LongCounter callStatusCounter = resource.callStatusCounter();
+ if (callStatusCounter == null) {
+ return;
+ }
+
+ Attributes attributes =
+ Attributes.of(CHANNEL_NAME, channelName, STATUS_CODE, statusCode.toString());
+
+ callStatusCounter.add(1, attributes);
+ }
+
+ void reportProbeResult(String channelName, String result) {
+ if (result == null) {
+ return;
+ }
+
+ LongCounter probeResultCounter = resource.probeResultCounter();
+ if (probeResultCounter != null) {
+
+ Attributes attributes =
+ Attributes.of(
+ CHANNEL_NAME, channelName,
+ PROBE_RESULT, result);
+
+ probeResultCounter.add(1, attributes);
+ }
+
+ DoubleGauge downtimeGauge = resource.channelDowntimeGauge();
+ if (downtimeGauge == null) {
+ return;
+ }
+
+ Attributes attributes = Attributes.of(CHANNEL_NAME, channelName);
+
+ if (result.isEmpty()) {
+ firstFailure.remove(channelName);
+ downtimeGauge.set(0, attributes);
+ } else {
+ firstFailure.putIfAbsent(channelName, System.nanoTime());
+ downtimeGauge.set(
+ (double) (System.nanoTime() - firstFailure.get(channelName)) / 1_000_000_000, attributes);
+ }
+ }
+
+ void reportCurrentChannel(String channelName, boolean current) {
+ channelActive.put(channelName, current);
+ }
+
+ void reportFallback(String fromChannelName, String toChannelName) {
+ LongCounter fallbackCounter = resource.fallbackCounter();
+ if (fallbackCounter == null) {
+ return;
+ }
+
+ Attributes attributes =
+ Attributes.of(
+ FROM_CHANNEL_NAME, fromChannelName,
+ TO_CHANNEL_NAME, toChannelName);
+
+ fallbackCounter.add(1, attributes);
+ }
+}
diff --git a/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsResource.java b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsResource.java
new file mode 100644
index 0000000000..2590ecf292
--- /dev/null
+++ b/google-cloud-bigtable/src/main/java/com/google/cloud/bigtable/gaxx/grpc/fallback/OpenTelemetryMetricsResource.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 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
+ *
+ * https://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.bigtable.gaxx.grpc.fallback;
+
+import com.google.auto.value.AutoValue;
+import com.google.cloud.bigtable.gaxx.grpc.fallback.AutoValue_OpenTelemetryMetricsResource.Builder;
+import io.opentelemetry.api.metrics.DoubleGauge;
+import io.opentelemetry.api.metrics.LongCounter;
+import io.opentelemetry.api.metrics.ObservableLongUpDownCounter;
+import javax.annotation.Nullable;
+
+@AutoValue
+abstract class OpenTelemetryMetricsResource {
+
+ @Nullable
+ abstract ObservableLongUpDownCounter currentChannelCounter();
+
+ @Nullable
+ abstract LongCounter fallbackCounter();
+
+ @Nullable
+ abstract LongCounter callStatusCounter();
+
+ @Nullable
+ abstract DoubleGauge errorRatioGauge();
+
+ @Nullable
+ abstract LongCounter probeResultCounter();
+
+ @Nullable
+ abstract DoubleGauge channelDowntimeGauge();
+
+ static Builder builder() {
+ return new AutoValue_OpenTelemetryMetricsResource.Builder();
+ }
+
+ @AutoValue.Builder
+ abstract static class Builder {
+ abstract Builder currentChannelCounter(ObservableLongUpDownCounter counter);
+
+ abstract Builder fallbackCounter(LongCounter counter);
+
+ abstract Builder callStatusCounter(LongCounter counter);
+
+ abstract Builder errorRatioGauge(DoubleGauge gauge);
+
+ abstract Builder probeResultCounter(LongCounter counter);
+
+ abstract Builder channelDowntimeGauge(DoubleGauge gauge);
+
+ abstract OpenTelemetryMetricsResource build();
+ }
+}
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClientTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClientTest.java
index ab2d542080..d99725029d 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClientTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableInstanceAdminClientTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClientTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClientTest.java
index 49ffea6786..3477fc053d 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClientTest.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/BaseBigtableTableAdminClientTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdmin.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdmin.java
index e1b18af722..643504c3c8 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdmin.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdmin.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdminImpl.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdminImpl.java
index 810c0b7601..7a1d8d08a0 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdminImpl.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableInstanceAdminImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdmin.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdmin.java
index 0df8357a13..384f5a2d87 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdmin.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdmin.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdminImpl.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdminImpl.java
index 44e3472650..a2fe476ea7 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdminImpl.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/admin/v2/MockBigtableTableAdminImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2025 Google LLC
+ * Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
index d84e56b342..c178d38816 100644
--- a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/data/v2/it/ExecuteQueryIT.java
@@ -179,15 +179,16 @@ public void allTypes() throws Exception {
try {
preparedStatement =
dataClient.prepareStatement(
- "SELECT 'stringVal' AS strCol, b'foo' as bytesCol, 1 AS intCol, CAST(1.2 AS FLOAT32) as"
- + " f32Col, CAST(1.3 AS FLOAT64) as f64Col, true as boolCol,"
+ "SELECT 'stringVal' AS strCol, b'foo' as bytesCol, 1 AS intCol, CAST(1.2 AS"
+ + " FLOAT32) as f32Col, CAST(1.3 AS FLOAT64) as f64Col, true as boolCol,"
+ " TIMESTAMP_FROM_UNIX_MILLIS(1000) AS tsCol, DATE(2024, 06, 01) as dateCol,"
+ " STRUCT(1 as a, \"foo\" as b) AS structCol, [1,2,3] AS arrCol, "
+ cf
+ " as mapCol, "
+ " CAST(b'\022\005Lover' AS `"
+ schemaBundleId
- + ".com.google.cloud.bigtable.data.v2.test.Album`) as protoCol, CAST('JAZZ' AS `"
+ + ".com.google.cloud.bigtable.data.v2.test.Album`) as protoCol, CAST('JAZZ' AS"
+ + " `"
+ schemaBundleId
+ ".com.google.cloud.bigtable.data.v2.test.Genre`) as enumCol FROM `"
+ tableId
diff --git a/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackChannelTest.java b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackChannelTest.java
new file mode 100644
index 0000000000..ba1683109b
--- /dev/null
+++ b/google-cloud-bigtable/src/test/java/com/google/cloud/bigtable/gaxx/grpc/fallback/GcpFallbackChannelTest.java
@@ -0,0 +1,1299 @@
+/*
+ * Copyright 2026 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
+ *
+ * https://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.bigtable.gaxx.grpc.fallback;
+
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackChannel.INIT_FAILURE_REASON;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CALL_STATUS_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CHANNEL_NAME;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.CURRENT_CHANNEL_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.ERROR_RATIO_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.FALLBACK_COUNT_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.FROM_CHANNEL_NAME;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.METRIC_PREFIX;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.PROBE_RESULT;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.PROBE_RESULT_METRIC;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.STATUS_CODE;
+import static com.google.cloud.bigtable.gaxx.grpc.fallback.GcpFallbackOpenTelemetry.TO_CHANNEL_NAME;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import io.grpc.CallOptions;
+import io.grpc.Channel;
+import io.grpc.ClientCall;
+import io.grpc.ManagedChannel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.MethodDescriptor.Marshaller;
+import io.grpc.Status;
+import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.metrics.InstrumentType;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.opentelemetry.sdk.metrics.data.AggregationTemporality;
+import io.opentelemetry.sdk.metrics.data.DoublePointData;
+import io.opentelemetry.sdk.metrics.data.LongPointData;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+import javax.annotation.Nonnull;
+import net.jcip.annotations.NotThreadSafe;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+
+@NotThreadSafe
+@RunWith(JUnit4.class)
+public class GcpFallbackChannelTest {
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.WARN);
+
+ static class DummyMarshaller implements Marshaller