diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
index 1a6726b9c01b..e2415c798154 100644
--- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
+++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
@@ -100,6 +100,7 @@
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.ProtoUtils;
import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.metrics.MeterProvider;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
@@ -122,6 +123,7 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
/**
* @since 2.14.0
@@ -432,6 +434,10 @@ public int hashCode() {
grpcInterceptorProvider,
blobWriteSessionConfig,
openTelemetry,
+ enableOtelMetrics,
+ enableOtelDebugMetrics,
+ getMeterProvider(),
+ getMetricInterval(),
baseHashCode());
}
@@ -446,11 +452,15 @@ public boolean equals(Object o) {
GrpcStorageOptions that = (GrpcStorageOptions) o;
return attemptDirectPath == that.attemptDirectPath
&& enableGrpcClientMetrics == that.enableGrpcClientMetrics
+ && enableOtelMetrics == that.enableOtelMetrics
+ && enableOtelDebugMetrics == that.enableOtelDebugMetrics
&& Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager)
&& Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration)
&& Objects.equals(grpcInterceptorProvider, that.grpcInterceptorProvider)
&& Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig)
&& Objects.equals(openTelemetry, that.openTelemetry)
+ && Objects.equals(getMeterProvider(), that.getMeterProvider())
+ && Objects.equals(getMetricInterval(), that.getMetricInterval())
&& this.baseEquals(that);
}
@@ -747,6 +757,64 @@ public GrpcStorageOptions.Builder setApiTracerFactory(ApiTracerFactory apiTracer
public GrpcStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) {
requireNonNull(openTelemetry, "openTelemetry must be non null");
this.openTelemetry = openTelemetry;
+ this.customOpenTelemetryConfigured = true;
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry client metrics.
+ *
+ * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public GrpcStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) {
+ super.setEnableOtelMetrics(enableOtelMetrics);
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry debug client metrics.
+ *
+ * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) {
+ super.setEnableOtelDebugMetrics(enableOtelDebugMetrics);
+ return this;
+ }
+
+ /**
+ * Set a custom {@link MeterProvider} for recording client metrics.
+ *
+ * @param meterProvider custom MeterProvider to use, or null to use the default
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) {
+ super.setMeterProvider(meterProvider);
+ return this;
+ }
+
+ /**
+ * Set the metric export interval for periodic metric reading.
+ *
+ *
When using the default Cloud Monitoring exporter, the interval must be at least 60 seconds
+ * (1 minute). Intervals less than 60 seconds are permitted only when a custom {@link
+ * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit
+ * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}.
+ *
+ * @param metricInterval interval duration
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public GrpcStorageOptions.Builder setMetricInterval(Duration metricInterval) {
+ super.setMetricInterval(metricInterval);
return this;
}
diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
index dac8a010cdfa..ee3e73487464 100644
--- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
+++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/HttpStorageOptions.java
@@ -42,13 +42,16 @@
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableSet;
import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.metrics.MeterProvider;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.time.Clock;
+import java.time.Duration;
import java.util.Objects;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
/**
* @since 2.14.0
@@ -110,7 +113,14 @@ public HttpStorageOptions.Builder toBuilder() {
@Override
public int hashCode() {
return Objects.hash(
- retryAlgorithmManager, blobWriteSessionConfig, openTelemetry, baseHashCode());
+ retryAlgorithmManager,
+ blobWriteSessionConfig,
+ openTelemetry,
+ enableOtelMetrics,
+ enableOtelDebugMetrics,
+ getMeterProvider(),
+ getMetricInterval(),
+ baseHashCode());
}
@Override
@@ -125,6 +135,10 @@ public boolean equals(Object o) {
return Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager)
&& Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig)
&& Objects.equals(openTelemetry, that.openTelemetry)
+ && enableOtelMetrics == that.enableOtelMetrics
+ && enableOtelDebugMetrics == that.enableOtelDebugMetrics
+ && Objects.equals(getMeterProvider(), that.getMeterProvider())
+ && Objects.equals(getMetricInterval(), that.getMetricInterval())
&& this.baseEquals(that);
}
@@ -315,6 +329,64 @@ public HttpStorageOptions build() {
public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) {
requireNonNull(openTelemetry, "openTelemetry must be non null");
this.openTelemetry = openTelemetry;
+ this.customOpenTelemetryConfigured = true;
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry client metrics.
+ *
+ * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public HttpStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) {
+ super.setEnableOtelMetrics(enableOtelMetrics);
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry debug client metrics.
+ *
+ * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) {
+ super.setEnableOtelDebugMetrics(enableOtelDebugMetrics);
+ return this;
+ }
+
+ /**
+ * Set a custom {@link MeterProvider} for recording client metrics.
+ *
+ * @param meterProvider custom MeterProvider to use, or null to use the default
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) {
+ super.setMeterProvider(meterProvider);
+ return this;
+ }
+
+ /**
+ * Set the metric export interval for periodic metric reading.
+ *
+ *
When using the default Cloud Monitoring exporter, the interval must be at least 60 seconds
+ * (1 minute). Intervals less than 60 seconds are permitted only when a custom {@link
+ * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit
+ * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}.
+ *
+ * @param metricInterval interval duration
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ @Override
+ public HttpStorageOptions.Builder setMetricInterval(Duration metricInterval) {
+ super.setMetricInterval(metricInterval);
return this;
}
}
diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java
new file mode 100644
index 000000000000..30be2ba85ef2
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java
@@ -0,0 +1,115 @@
+/*
+ * 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
+ *
+ * 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.storage;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Locale;
+import java.util.function.Function;
+import java.util.logging.Logger;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+final class StorageMetricsConfig {
+ private static final Logger LOGGER = Logger.getLogger(StorageMetricsConfig.class.getName());
+
+ static final String ENV_ENABLE_OTEL_METRICS_JAVA = "GCP_STORAGE_JAVA_ENABLE_OTEL_METRICS";
+ static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS";
+
+ private static volatile Function envResolver = System::getenv;
+
+ private StorageMetricsConfig() {}
+
+ static boolean isEnableOtelMetrics() {
+ return isEnableOtelMetrics(null);
+ }
+
+ static boolean isEnableOtelDebugMetrics() {
+ return isEnableOtelDebugMetrics(null);
+ }
+
+ static boolean isEnableOtelMetrics(@Nullable Boolean builderSetting) {
+ return isEnableOtelMetrics(envResolver, builderSetting);
+ }
+
+ static boolean isEnableOtelDebugMetrics(@Nullable Boolean builderSetting) {
+ return isEnableOtelDebugMetrics(envResolver, builderSetting);
+ }
+
+ @VisibleForTesting
+ static boolean isEnableOtelMetrics(
+ Function env, @Nullable Boolean builderSetting) {
+ if (builderSetting != null) {
+ return builderSetting;
+ }
+ String javaEnv = env.apply(ENV_ENABLE_OTEL_METRICS_JAVA);
+ Boolean parsed = parseBooleanValue(javaEnv, ENV_ENABLE_OTEL_METRICS_JAVA);
+ if (parsed != null) {
+ return parsed;
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ static boolean isEnableOtelDebugMetrics(
+ Function env, @Nullable Boolean builderSetting) {
+ if (builderSetting != null) {
+ return builderSetting;
+ }
+ String javaEnv = env.apply(ENV_ENABLE_OTEL_DEBUG_METRICS);
+ Boolean parsed = parseBooleanValue(javaEnv, ENV_ENABLE_OTEL_DEBUG_METRICS);
+ if (parsed != null) {
+ return parsed;
+ }
+ return false;
+ }
+
+ @Nullable
+ @VisibleForTesting
+ static Boolean parseBooleanValue(@Nullable String val, String varName) {
+ if (val == null) {
+ return null;
+ }
+ String normalized = val.trim().toLowerCase(Locale.ENGLISH);
+ switch (normalized) {
+ case "1":
+ case "t":
+ case "true":
+ return Boolean.TRUE;
+ case "0":
+ case "f":
+ case "false":
+ return Boolean.FALSE;
+ default:
+ LOGGER.warning(
+ "Unrecognized boolean value '"
+ + val
+ + "' for "
+ + varName
+ + "; ignoring and falling back to builder/default");
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ static void setEnvResolverForTesting(Function testEnv) {
+ envResolver = testEnv != null ? testEnv : System::getenv;
+ }
+
+ @VisibleForTesting
+ static void resetResolversForTesting() {
+ envResolver = System::getenv;
+ }
+}
diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
index 97eecedaeef1..2d123041ca20 100644
--- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
+++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageOptions.java
@@ -16,6 +16,8 @@
package com.google.cloud.storage;
+import static com.google.common.base.Preconditions.checkArgument;
+
import com.google.api.core.BetaApi;
import com.google.api.core.InternalApi;
import com.google.cloud.NoCredentials;
@@ -33,11 +35,16 @@
import com.google.cloud.storage.TransportCompatibility.Transport;
import com.google.cloud.storage.spi.StorageRpcFactory;
import io.opentelemetry.api.OpenTelemetry;
+import io.opentelemetry.api.metrics.MeterProvider;
import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.time.Duration;
import java.util.Locale;
import java.util.Properties;
+import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
public abstract class StorageOptions extends ServiceOptions {
@@ -112,12 +119,25 @@ public DefaultStorageRpcFactory() {
public abstract static class Builder
extends ServiceOptions.Builder {
+ @Nullable Boolean enableOtelMetrics = null;
+ @Nullable Boolean enableOtelDebugMetrics = null;
+ @Nullable MeterProvider meterProvider = null;
+ Duration metricInterval = Duration.ofSeconds(60);
+ boolean customOpenTelemetryConfigured = false;
+ boolean customMeterProviderConfigured = false;
+
Builder() {
setUseJwtAccessWithScope(false);
}
Builder(StorageOptions options) {
super(options);
+ this.enableOtelMetrics = options.rawEnableOtelMetrics;
+ this.enableOtelDebugMetrics = options.rawEnableOtelDebugMetrics;
+ this.meterProvider = options.meterProvider;
+ this.metricInterval = options.metricInterval;
+ this.customMeterProviderConfigured = options.customMeterProviderConfigured;
+ this.customOpenTelemetryConfigured = options.customOpenTelemetryConfigured;
}
public abstract Builder setStorageRetryStrategy(StorageRetryStrategy storageRetryStrategy);
@@ -143,12 +163,106 @@ public abstract StorageOptions.Builder setBlobWriteSessionConfig(
@BetaApi
public abstract StorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry);
+ /**
+ * Enable or disable OpenTelemetry client metrics.
+ *
+ * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) {
+ this.enableOtelMetrics = enableOtelMetrics;
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry debug client metrics.
+ *
+ * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public StorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) {
+ this.enableOtelDebugMetrics = enableOtelDebugMetrics;
+ return this;
+ }
+
+ /**
+ * Set a custom {@link MeterProvider} for recording client metrics.
+ *
+ * @param meterProvider custom MeterProvider to use, or null to use the default
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public StorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) {
+ this.meterProvider = meterProvider;
+ this.customMeterProviderConfigured = meterProvider != null;
+ return this;
+ }
+
+ /**
+ * Set the metric export interval for periodic metric reading.
+ *
+ * When using the default Cloud Monitoring exporter, the interval must be at least 60 seconds
+ * (1 minute). Intervals less than 60 seconds are permitted only when a custom {@link
+ * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit
+ * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}.
+ *
+ * @param metricInterval interval duration
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public StorageOptions.Builder setMetricInterval(Duration metricInterval) {
+ checkArgument(
+ metricInterval != null && !metricInterval.isNegative() && !metricInterval.isZero(),
+ "metricInterval must be positive");
+ this.metricInterval = metricInterval;
+ return this;
+ }
+
@Override
public abstract StorageOptions build();
}
+ private static final Logger LOGGER = Logger.getLogger(StorageOptions.class.getName());
+
+ final boolean enableOtelMetrics;
+ final boolean enableOtelDebugMetrics;
+ @Nullable final Boolean rawEnableOtelMetrics;
+ @Nullable final Boolean rawEnableOtelDebugMetrics;
+ transient MeterProvider meterProvider;
+ final Duration metricInterval;
+ final boolean customMeterProviderConfigured;
+ final boolean customOpenTelemetryConfigured;
+
StorageOptions(Builder builder, StorageDefaults serviceDefaults) {
super(StorageFactory.class, StorageRpcFactory.class, builder, serviceDefaults);
+ this.rawEnableOtelMetrics = builder.enableOtelMetrics;
+ this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics;
+ this.enableOtelMetrics = StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics);
+ this.enableOtelDebugMetrics =
+ StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics);
+ this.meterProvider = builder.meterProvider;
+ this.customMeterProviderConfigured =
+ builder.customMeterProviderConfigured || builder.meterProvider != null;
+ this.customOpenTelemetryConfigured = builder.customOpenTelemetryConfigured;
+ this.metricInterval =
+ builder.metricInterval != null ? builder.metricInterval : Duration.ofSeconds(60);
+ boolean hasCustomOtelOrMeterProvider =
+ this.customMeterProviderConfigured || this.customOpenTelemetryConfigured;
+ if (!hasCustomOtelOrMeterProvider && this.metricInterval != null) {
+ checkArgument(
+ this.metricInterval.compareTo(Duration.ofSeconds(60)) >= 0,
+ "Metric export interval cannot be less than 60 seconds when using Cloud Monitoring exporter");
+ }
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+ in.defaultReadObject();
+ if (this.customMeterProviderConfigured || this.meterProvider != null) {
+ LOGGER.warning(
+ "Custom MeterProvider is transient and cannot be serialized across workers; falling back to default provider.");
+ }
}
abstract static class StorageDefaults implements ServiceDefaults {}
@@ -185,6 +299,46 @@ public static String version() {
@BetaApi
public abstract OpenTelemetry getOpenTelemetry();
+ /**
+ * Whether OpenTelemetry client metrics are enabled.
+ *
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public boolean isEnableOtelMetrics() {
+ return enableOtelMetrics;
+ }
+
+ /**
+ * Whether OpenTelemetry debug client metrics are enabled.
+ *
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public boolean isEnableOtelDebugMetrics() {
+ return enableOtelDebugMetrics;
+ }
+
+ /**
+ * The {@link MeterProvider} configured for recording client metrics.
+ *
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public @Nullable MeterProvider getMeterProvider() {
+ return meterProvider;
+ }
+
+ /**
+ * The metric export interval configured for periodic metric reading.
+ *
+ * @since 2.74.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Duration getMetricInterval() {
+ return metricInterval != null ? metricInterval : Duration.ofSeconds(60);
+ }
+
@SuppressWarnings("unchecked")
@Override
public abstract StorageOptions.Builder toBuilder();
diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageMetricsConfigTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageMetricsConfigTest.java
new file mode 100644
index 000000000000..5cb4f3946490
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageMetricsConfigTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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
+ *
+ * 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.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Test;
+
+public final class StorageMetricsConfigTest {
+
+ @After
+ public void tearDown() {
+ StorageMetricsConfig.resetResolversForTesting();
+ }
+
+ @Test
+ public void isEnableOtelMetrics_precedenceOrder() {
+ Map env = new HashMap<>();
+
+ // 1. Explicit programmatic builder setting takes precedence over environment variable
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isFalse();
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false");
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isTrue();
+
+ // 2. When builder setting is null, environment variable is respected
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isTrue();
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false");
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse();
+
+ // 3. When both builder setting and env var are absent/null, default is false
+ env.clear();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse();
+ }
+
+ @Test
+ public void isEnableOtelDebugMetrics_precedenceOrder() {
+ Map env = new HashMap<>();
+
+ // 1. Explicit programmatic builder setting takes precedence over environment variable
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, false)).isFalse();
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "false");
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, true)).isTrue();
+
+ // 2. When builder setting is null, environment variable is respected
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, null)).isTrue();
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "false");
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, null)).isFalse();
+
+ // 3. When both builder setting and env var are absent/null, default is false
+ env.clear();
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, null)).isFalse();
+ }
+
+ @Test
+ public void parseBooleanValue_handlesPermissiveValues() {
+ String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "};
+ for (String val : trueValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isTrue();
+ }
+
+ String[] falseValues = {"0", "f", "F", "false", "FALSE", "False", " false "};
+ for (String val : falseValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isFalse();
+ }
+
+ String[] invalidValues = {"invalid", "2", "yes", "no", "enabled", ""};
+ for (String val : invalidValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isNull();
+ }
+
+ assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull();
+ }
+
+ @Test
+ public void setEnvResolverForTesting_resolution() {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics()).isTrue();
+ assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics()).isTrue();
+
+ StorageMetricsConfig.resetResolversForTesting();
+ }
+}
diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
new file mode 100644
index 000000000000..a54b9a01dba3
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
@@ -0,0 +1,448 @@
+/*
+ * 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
+ *
+ * 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.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import io.opentelemetry.api.metrics.MeterProvider;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Field;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.After;
+import org.junit.Test;
+
+public final class StorageOptionsTest {
+
+ @After
+ public void tearDown() {
+ StorageMetricsConfig.resetResolversForTesting();
+ }
+
+ @Test
+ public void defaultState_metricsDisabled() {
+ HttpStorageOptions httpOptions = HttpStorageOptions.http().build();
+ assertThat(httpOptions.isEnableOtelMetrics()).isFalse();
+ assertThat(httpOptions.isEnableOtelDebugMetrics()).isFalse();
+ assertThat(httpOptions.getMeterProvider()).isNull();
+ assertThat(httpOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+
+ GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build();
+ assertThat(grpcOptions.isEnableOtelMetrics()).isFalse();
+ assertThat(grpcOptions.isEnableOtelDebugMetrics()).isFalse();
+ assertThat(grpcOptions.getMeterProvider()).isNull();
+ assertThat(grpcOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ }
+
+ @Test
+ public void builder_explicitEnabling() {
+ MeterProvider mockMeterProvider = SdkMeterProvider.builder().build();
+ Duration interval = Duration.ofSeconds(30);
+
+ HttpStorageOptions httpOptions =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .setMeterProvider(mockMeterProvider)
+ .setMetricInterval(interval)
+ .build();
+ assertThat(httpOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(httpOptions.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(httpOptions.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(httpOptions.getMetricInterval()).isEqualTo(interval);
+
+ HttpStorageOptions rebuiltHttp = httpOptions.toBuilder().build();
+ assertThat(rebuiltHttp.isEnableOtelMetrics()).isTrue();
+ assertThat(rebuiltHttp.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(rebuiltHttp.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(rebuiltHttp.getMetricInterval()).isEqualTo(interval);
+
+ GrpcStorageOptions grpcOptions =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .setMeterProvider(mockMeterProvider)
+ .setMetricInterval(interval)
+ .build();
+ assertThat(grpcOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcOptions.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(grpcOptions.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(grpcOptions.getMetricInterval()).isEqualTo(interval);
+
+ GrpcStorageOptions rebuiltGrpc = grpcOptions.toBuilder().build();
+ assertThat(rebuiltGrpc.isEnableOtelMetrics()).isTrue();
+ assertThat(rebuiltGrpc.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(rebuiltGrpc.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(rebuiltGrpc.getMetricInterval()).isEqualTo(interval);
+ }
+
+ @Test
+ public void precedence_builderTakesPrecedenceOverEnv() {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ HttpStorageOptions httpDisabled =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(false)
+ .build();
+ assertThat(httpDisabled.isEnableOtelMetrics()).isFalse();
+ assertThat(httpDisabled.isEnableOtelDebugMetrics()).isFalse();
+
+ GrpcStorageOptions grpcDisabled =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(false)
+ .build();
+ assertThat(grpcDisabled.isEnableOtelMetrics()).isFalse();
+ assertThat(grpcDisabled.isEnableOtelDebugMetrics()).isFalse();
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "false");
+
+ HttpStorageOptions httpEnabled =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(httpEnabled.isEnableOtelMetrics()).isTrue();
+ assertThat(httpEnabled.isEnableOtelDebugMetrics()).isTrue();
+
+ GrpcStorageOptions grpcEnabled =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(grpcEnabled.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcEnabled.isEnableOtelDebugMetrics()).isTrue();
+ }
+
+ @Test
+ public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() {
+ Map env = new HashMap<>();
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ HttpStorageOptions httpEnabled =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(httpEnabled.isEnableOtelMetrics()).isTrue();
+ assertThat(httpEnabled.isEnableOtelDebugMetrics()).isTrue();
+
+ GrpcStorageOptions grpcEnabled =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(grpcEnabled.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcEnabled.isEnableOtelDebugMetrics()).isTrue();
+
+ HttpStorageOptions httpDisabled =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(false)
+ .build();
+ assertThat(httpDisabled.isEnableOtelMetrics()).isFalse();
+ assertThat(httpDisabled.isEnableOtelDebugMetrics()).isFalse();
+
+ GrpcStorageOptions grpcDisabled =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(false)
+ .build();
+ assertThat(grpcDisabled.isEnableOtelMetrics()).isFalse();
+ assertThat(grpcDisabled.isEnableOtelDebugMetrics()).isFalse();
+ }
+
+ @Test
+ public void permissiveBooleanParsing() {
+ String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "};
+ for (String val : trueValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isTrue();
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, val);
+ // When builderSetting is null, env var is parsed and respected
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isTrue();
+ // When builderSetting is provided, builder takes precedence over env var
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isFalse();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isTrue();
+ }
+
+ String[] falseValues = {"0", "f", "F", "false", "FALSE", "False", " false "};
+ for (String val : falseValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isFalse();
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, val);
+ // When builderSetting is null, env var is parsed and respected
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse();
+ // When builderSetting is provided, builder takes precedence over env var
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isTrue();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isFalse();
+ }
+
+ String[] invalidValues = {"invalid", "2", "yes", "no", "enabled", ""};
+ for (String val : invalidValues) {
+ assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isNull();
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, val);
+ // Unrecognized values fall back to builder setting if provided
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isTrue();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isFalse();
+ // When builder setting is null, falls back to default false
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse();
+ }
+
+ // When both are absent/null, default is false
+ Map emptyEnv = new HashMap<>();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(emptyEnv::get, null)).isFalse();
+ assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull();
+ }
+
+ @Test
+ public void toBuilder_doesNotBakeInEnvironmentVariable() {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ HttpStorageOptions httpOptions = HttpStorageOptions.http().build();
+ assertThat(httpOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(httpOptions.isEnableOtelDebugMetrics()).isTrue();
+
+ // Clear environment variable and rebuild
+ env.clear();
+ HttpStorageOptions rebuiltHttp = httpOptions.toBuilder().build();
+ assertThat(rebuiltHttp.isEnableOtelMetrics()).isFalse();
+ assertThat(rebuiltHttp.isEnableOtelDebugMetrics()).isFalse();
+
+ // Repeat for GrpcStorageOptions
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build();
+ assertThat(grpcOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcOptions.isEnableOtelDebugMetrics()).isTrue();
+
+ env.clear();
+ GrpcStorageOptions rebuiltGrpc = grpcOptions.toBuilder().build();
+ assertThat(rebuiltGrpc.isEnableOtelMetrics()).isFalse();
+ assertThat(rebuiltGrpc.isEnableOtelDebugMetrics()).isFalse();
+ }
+
+ @Test
+ public void environmentVariableJava() {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ HttpStorageOptions httpOptions = HttpStorageOptions.http().build();
+ assertThat(httpOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(httpOptions.isEnableOtelDebugMetrics()).isTrue();
+
+ GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build();
+ assertThat(grpcOptions.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcOptions.isEnableOtelDebugMetrics()).isTrue();
+ }
+
+ @Test
+ public void setMetricInterval_validation() {
+ MeterProvider customMeterProvider = SdkMeterProvider.builder().build();
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(-1)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> HttpStorageOptions.http().setMetricInterval(Duration.ZERO));
+ assertThrows(
+ IllegalArgumentException.class, () -> HttpStorageOptions.http().setMetricInterval(null));
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(-1)));
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ZERO));
+ assertThrows(
+ IllegalArgumentException.class, () -> GrpcStorageOptions.grpc().setMetricInterval(null));
+
+ // Reject intervals < 60s when meterProvider == null (SDK-owned Cloud Monitoring exporter)
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(59)).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(30)).build());
+
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(59)).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(30)).build());
+
+ // Accept intervals >= 60s when meterProvider == null
+ HttpStorageOptions http60 =
+ HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(60)).build();
+ assertThat(http60.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+
+ GrpcStorageOptions grpc60 =
+ GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(60)).build();
+ assertThat(grpc60.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+
+ // Accept intervals < 60s when custom meterProvider is configured
+ HttpStorageOptions httpCustom =
+ HttpStorageOptions.http()
+ .setMeterProvider(customMeterProvider)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(httpCustom.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+
+ GrpcStorageOptions grpcCustom =
+ GrpcStorageOptions.grpc()
+ .setMeterProvider(customMeterProvider)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(grpcCustom.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+
+ // Accept intervals < 60s when explicit openTelemetry is configured
+ io.opentelemetry.api.OpenTelemetry customOtel = io.opentelemetry.api.OpenTelemetry.noop();
+ HttpStorageOptions httpCustomOtel =
+ HttpStorageOptions.http()
+ .setOpenTelemetry(customOtel)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(httpCustomOtel.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+
+ GrpcStorageOptions grpcCustomOtel =
+ GrpcStorageOptions.grpc()
+ .setOpenTelemetry(customOtel)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(grpcCustomOtel.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+ }
+
+ @Test
+ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws Exception {
+ MeterProvider customMeterProvider = SdkMeterProvider.builder().build();
+ HttpStorageOptions httpOptions =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(true)
+ .setMeterProvider(customMeterProvider)
+ .build();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(httpOptions);
+ }
+
+ HttpStorageOptions deserializedHttp;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserializedHttp = (HttpStorageOptions) ois.readObject();
+ }
+
+ assertThat(deserializedHttp).isNotNull();
+ assertThat(deserializedHttp.isEnableOtelMetrics()).isTrue();
+ // Transient meterProvider does not carry over; returns null
+ assertThat(deserializedHttp.getMeterProvider()).isNull();
+
+ GrpcStorageOptions grpcOptions =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(true)
+ .setMeterProvider(customMeterProvider)
+ .build();
+
+ baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(grpcOptions);
+ }
+
+ GrpcStorageOptions deserializedGrpc;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserializedGrpc = (GrpcStorageOptions) ois.readObject();
+ }
+
+ assertThat(deserializedGrpc).isNotNull();
+ assertThat(deserializedGrpc.isEnableOtelMetrics()).isTrue();
+ // Transient meterProvider does not carry over; returns null
+ assertThat(deserializedGrpc.getMeterProvider()).isNull();
+ }
+
+ @Test
+ public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throws Exception {
+ Field httpBuilderField = StorageOptions.Builder.class.getDeclaredField("metricInterval");
+ httpBuilderField.setAccessible(true);
+ HttpStorageOptions.Builder httpBuilder = HttpStorageOptions.http();
+ httpBuilderField.set(httpBuilder, null);
+ HttpStorageOptions httpWithNullInterval = httpBuilder.build();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(httpWithNullInterval);
+ }
+
+ HttpStorageOptions deserializedHttp;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserializedHttp = (HttpStorageOptions) ois.readObject();
+ }
+
+ assertThat(deserializedHttp).isNotNull();
+ // Verify NPE guard defaults to 60 seconds
+ assertThat(deserializedHttp.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ // Verify equals and hashCode match a default instance
+ HttpStorageOptions defaultHttp = HttpStorageOptions.http().build();
+ assertThat(deserializedHttp).isEqualTo(defaultHttp);
+ assertThat(defaultHttp).isEqualTo(deserializedHttp);
+ assertThat(deserializedHttp.hashCode()).isEqualTo(defaultHttp.hashCode());
+
+ Field grpcBuilderField = StorageOptions.Builder.class.getDeclaredField("metricInterval");
+ grpcBuilderField.setAccessible(true);
+ GrpcStorageOptions.Builder grpcBuilder = GrpcStorageOptions.grpc();
+ grpcBuilderField.set(grpcBuilder, null);
+ GrpcStorageOptions grpcWithNullInterval = grpcBuilder.build();
+
+ baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(grpcWithNullInterval);
+ }
+
+ GrpcStorageOptions deserializedGrpc;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserializedGrpc = (GrpcStorageOptions) ois.readObject();
+ }
+
+ assertThat(deserializedGrpc).isNotNull();
+ assertThat(deserializedGrpc.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ GrpcStorageOptions defaultGrpc = GrpcStorageOptions.grpc().build();
+ assertThat(deserializedGrpc).isEqualTo(defaultGrpc);
+ assertThat(defaultGrpc).isEqualTo(deserializedGrpc);
+ assertThat(deserializedGrpc.hashCode()).isEqualTo(defaultGrpc.hashCode());
+ }
+}