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..0793d4614223 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,
+ isEnableOtelMetrics(),
+ isEnableOtelDebugMetrics(),
+ getMeterProvider(),
+ getMetricInterval(),
baseHashCode());
}
@@ -446,11 +452,15 @@ public boolean equals(Object o) {
GrpcStorageOptions that = (GrpcStorageOptions) o;
return attemptDirectPath == that.attemptDirectPath
&& enableGrpcClientMetrics == that.enableGrpcClientMetrics
+ && isEnableOtelMetrics() == that.isEnableOtelMetrics()
+ && isEnableOtelDebugMetrics() == that.isEnableOtelDebugMetrics()
&& 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;
+ setCustomOpenTelemetryConfigured(true);
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry client metrics.
+ *
+ * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled
+ * @since 2.76.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.76.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.76.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.76.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..8dff1c8185c1 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,
+ isEnableOtelMetrics(),
+ isEnableOtelDebugMetrics(),
+ 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)
+ && isEnableOtelMetrics() == that.isEnableOtelMetrics()
+ && isEnableOtelDebugMetrics() == that.isEnableOtelDebugMetrics()
+ && 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;
+ setCustomOpenTelemetryConfigured(true);
+ return this;
+ }
+
+ /**
+ * Enable or disable OpenTelemetry client metrics.
+ *
+ * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled
+ * @since 2.76.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.76.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.76.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.76.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..923d72d146b8
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java
@@ -0,0 +1,112 @@
+/*
+ * 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) {
+ return resolveFlag(env, builderSetting, ENV_ENABLE_OTEL_METRICS_JAVA);
+ }
+
+ @VisibleForTesting
+ static boolean isEnableOtelDebugMetrics(
+ Function env, @Nullable Boolean builderSetting) {
+ return resolveFlag(env, builderSetting, ENV_ENABLE_OTEL_DEBUG_METRICS);
+ }
+
+ private static boolean resolveFlag(
+ Function env, @Nullable Boolean builderSetting, String envVarName) {
+ if (builderSetting != null) {
+ return builderSetting;
+ }
+ String javaEnv = env.apply(envVarName);
+ Boolean parsed = parseBooleanValue(javaEnv, envVarName);
+ 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..672c9ce34041 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,53 @@ public DefaultStorageRpcFactory() {
public abstract static class Builder
extends ServiceOptions.Builder {
+ private @Nullable Boolean enableOtelMetrics = null;
+ private @Nullable Boolean enableOtelDebugMetrics = null;
+ private @Nullable MeterProvider meterProvider = null;
+ private Duration metricInterval = Duration.ofSeconds(60);
+ private boolean customOpenTelemetryConfigured = false;
+ private boolean customMeterProviderConfigured = false;
+
Builder() {
setUseJwtAccessWithScope(false);
}
Builder(StorageOptions options) {
super(options);
+ this.enableOtelMetrics = options.getRawEnableOtelMetrics();
+ this.enableOtelDebugMetrics = options.getRawEnableOtelDebugMetrics();
+ this.meterProvider = options.getMeterProvider();
+ this.metricInterval = options.metricInterval;
+ this.customMeterProviderConfigured = options.isCustomMeterProviderConfigured();
+ this.customOpenTelemetryConfigured = options.isCustomOpenTelemetryConfigured();
+ }
+
+ @Nullable Boolean getEnableOtelMetrics() {
+ return enableOtelMetrics;
+ }
+
+ @Nullable Boolean getEnableOtelDebugMetrics() {
+ return enableOtelDebugMetrics;
+ }
+
+ @Nullable MeterProvider getMeterProvider() {
+ return meterProvider;
+ }
+
+ Duration getMetricInterval() {
+ return metricInterval;
+ }
+
+ boolean isCustomOpenTelemetryConfigured() {
+ return customOpenTelemetryConfigured;
+ }
+
+ void setCustomOpenTelemetryConfigured(boolean customOpenTelemetryConfigured) {
+ this.customOpenTelemetryConfigured = customOpenTelemetryConfigured;
+ }
+
+ boolean isCustomMeterProviderConfigured() {
+ return customMeterProviderConfigured;
}
public abstract Builder setStorageRetryStrategy(StorageRetryStrategy storageRetryStrategy);
@@ -143,12 +191,124 @@ 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.76.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.76.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.76.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.76.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());
+
+ private final boolean enableOtelMetrics;
+ private final boolean enableOtelDebugMetrics;
+ @Nullable private final Boolean rawEnableOtelMetrics;
+ @Nullable private final Boolean rawEnableOtelDebugMetrics;
+ private transient MeterProvider meterProvider;
+ private final Duration metricInterval;
+ private final boolean customMeterProviderConfigured;
+ private final boolean customOpenTelemetryConfigured;
+
StorageOptions(Builder builder, StorageDefaults serviceDefaults) {
super(StorageFactory.class, StorageRpcFactory.class, builder, serviceDefaults);
+ this.rawEnableOtelMetrics = builder.getEnableOtelMetrics();
+ this.rawEnableOtelDebugMetrics = builder.getEnableOtelDebugMetrics();
+ this.enableOtelMetrics =
+ StorageMetricsConfig.isEnableOtelMetrics(builder.getEnableOtelMetrics());
+ this.enableOtelDebugMetrics =
+ this.enableOtelMetrics
+ && StorageMetricsConfig.isEnableOtelDebugMetrics(builder.getEnableOtelDebugMetrics());
+ this.meterProvider = builder.getMeterProvider();
+ this.customMeterProviderConfigured =
+ builder.isCustomMeterProviderConfigured() || builder.getMeterProvider() != null;
+ this.customOpenTelemetryConfigured = builder.isCustomOpenTelemetryConfigured();
+ this.metricInterval =
+ builder.getMetricInterval() != null ? builder.getMetricInterval() : 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");
+ }
+ }
+
+ @Nullable Boolean getRawEnableOtelMetrics() {
+ return rawEnableOtelMetrics;
+ }
+
+ @Nullable Boolean getRawEnableOtelDebugMetrics() {
+ return rawEnableOtelDebugMetrics;
+ }
+
+ boolean isCustomMeterProviderConfigured() {
+ return customMeterProviderConfigured;
+ }
+
+ boolean isCustomOpenTelemetryConfigured() {
+ return customOpenTelemetryConfigured;
+ }
+
+ 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 +345,52 @@ public static String version() {
@BetaApi
public abstract OpenTelemetry getOpenTelemetry();
+ /**
+ * Whether OpenTelemetry client metrics are enabled.
+ *
+ * @since 2.76.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.76.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.76.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.76.0 This new api is in preview and is subject to breaking changes.
+ */
+ @BetaApi
+ public Duration getMetricInterval() {
+ Duration interval = metricInterval != null ? metricInterval : Duration.ofSeconds(60);
+ if (meterProvider == null && !customOpenTelemetryConfigured) {
+ if (interval.compareTo(Duration.ofSeconds(60)) < 0) {
+ return Duration.ofSeconds(60);
+ }
+ }
+ return interval;
+ }
+
@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..3a44ef23dc10
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageMetricsConfigTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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 com.google.common.truth.Truth.assertWithMessage;
+
+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_explicitBuilderSettingTakesPrecedenceOverEnvVar() {
+ Map env = new HashMap<>();
+ 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();
+ }
+
+ @Test
+ public void isEnableOtelMetrics_envVarRespectedWhenBuilderSettingNull() {
+ Map env = new HashMap<>();
+ 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();
+ }
+
+ @Test
+ public void isEnableOtelMetrics_defaultsToFalseWhenUnset() {
+ Map env = new HashMap<>();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse();
+ }
+
+ @Test
+ public void isEnableOtelDebugMetrics_explicitBuilderSettingTakesPrecedenceOverEnvVar() {
+ Map env = new HashMap<>();
+ 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();
+ }
+
+ @Test
+ public void isEnableOtelDebugMetrics_envVarRespectedWhenBuilderSettingNull() {
+ Map env = new HashMap<>();
+ 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();
+ }
+
+ @Test
+ public void isEnableOtelDebugMetrics_defaultsToFalseWhenUnset() {
+ Map env = new HashMap<>();
+ 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) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR"))
+ .isTrue();
+ }
+
+ String[] falseValues = {"0", "f", "F", "false", "FALSE", "False", " false "};
+ for (String val : falseValues) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR"))
+ .isFalse();
+ }
+
+ String[] invalidValues = {"invalid", "2", "yes", "no", "enabled", ""};
+ for (String val : invalidValues) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(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();
+ }
+
+ @Test
+ public void debugMetricsGatedOnBaseMetrics_inStorageOptions() {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ // Base metrics unset in env (defaults to false) -> options evaluate debug metrics to false
+ HttpStorageOptions httpEnvDebugOnly = HttpStorageOptions.http().build();
+ assertThat(httpEnvDebugOnly.isEnableOtelMetrics()).isFalse();
+ assertThat(httpEnvDebugOnly.isEnableOtelDebugMetrics()).isFalse();
+
+ GrpcStorageOptions grpcEnvDebugOnly = GrpcStorageOptions.grpc().build();
+ assertThat(grpcEnvDebugOnly.isEnableOtelMetrics()).isFalse();
+ assertThat(grpcEnvDebugOnly.isEnableOtelDebugMetrics()).isFalse();
+
+ // Base metrics set to true in env -> options evaluate debug metrics to true
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ HttpStorageOptions httpEnvBoth = HttpStorageOptions.http().build();
+ assertThat(httpEnvBoth.isEnableOtelMetrics()).isTrue();
+ assertThat(httpEnvBoth.isEnableOtelDebugMetrics()).isTrue();
+
+ GrpcStorageOptions grpcEnvBoth = GrpcStorageOptions.grpc().build();
+ assertThat(grpcEnvBoth.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcEnvBoth.isEnableOtelDebugMetrics()).isTrue();
+
+ // Explicit builder disabling base metrics overrides env and keeps debug metrics false
+ HttpStorageOptions httpBuilderGated =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(httpBuilderGated.isEnableOtelMetrics()).isFalse();
+ assertThat(httpBuilderGated.isEnableOtelDebugMetrics()).isFalse();
+
+ GrpcStorageOptions grpcBuilderGated =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(false)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(grpcBuilderGated.isEnableOtelMetrics()).isFalse();
+ assertThat(grpcBuilderGated.isEnableOtelDebugMetrics()).isFalse();
+
+ // Explicit builder enabling both evaluates debug metrics to true
+ HttpStorageOptions httpBuilderBoth =
+ HttpStorageOptions.http()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(httpBuilderBoth.isEnableOtelMetrics()).isTrue();
+ assertThat(httpBuilderBoth.isEnableOtelDebugMetrics()).isTrue();
+
+ GrpcStorageOptions grpcBuilderBoth =
+ GrpcStorageOptions.grpc()
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .build();
+ assertThat(grpcBuilderBoth.isEnableOtelMetrics()).isTrue();
+ assertThat(grpcBuilderBoth.isEnableOtelDebugMetrics()).isTrue();
+ }
+}
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..f2dc6f3fda8d
--- /dev/null
+++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java
@@ -0,0 +1,461 @@
+/*
+ * 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 com.google.common.truth.Truth.assertWithMessage;
+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 {
+
+ @FunctionalInterface
+ private interface BuilderConsumer {
+ void accept(StorageOptions.Builder builder) throws Exception;
+ }
+
+ private static void forEachTransport(BuilderConsumer consumer) throws Exception {
+ consumer.accept(HttpStorageOptions.http());
+ consumer.accept(GrpcStorageOptions.grpc());
+ }
+
+ @After
+ public void tearDown() {
+ StorageMetricsConfig.resetResolversForTesting();
+ }
+
+ @Test
+ public void defaultState_metricsDisabled() throws Exception {
+ forEachTransport(
+ builder -> {
+ StorageOptions options = builder.build();
+ assertThat(options.isEnableOtelMetrics()).isFalse();
+ assertThat(options.isEnableOtelDebugMetrics()).isFalse();
+ assertThat(options.getMeterProvider()).isNull();
+ assertThat(options.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ });
+ }
+
+ @Test
+ public void builder_explicitEnabling() throws Exception {
+ MeterProvider mockMeterProvider = SdkMeterProvider.builder().build();
+ Duration interval = Duration.ofSeconds(30);
+
+ forEachTransport(
+ builder -> {
+ StorageOptions options =
+ builder
+ .setEnableOtelMetrics(true)
+ .setEnableOtelDebugMetrics(true)
+ .setMeterProvider(mockMeterProvider)
+ .setMetricInterval(interval)
+ .build();
+ assertThat(options.isEnableOtelMetrics()).isTrue();
+ assertThat(options.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(options.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(options.getMetricInterval()).isEqualTo(interval);
+
+ StorageOptions rebuilt = options.toBuilder().build();
+ assertThat(rebuilt.isEnableOtelMetrics()).isTrue();
+ assertThat(rebuilt.isEnableOtelDebugMetrics()).isTrue();
+ assertThat(rebuilt.getMeterProvider()).isSameInstanceAs(mockMeterProvider);
+ assertThat(rebuilt.getMetricInterval()).isEqualTo(interval);
+ });
+ }
+
+ @Test
+ public void precedence_builderTakesPrecedenceOverEnv() throws Exception {
+ 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);
+
+ forEachTransport(
+ builder -> {
+ StorageOptions disabled =
+ builder.setEnableOtelMetrics(false).setEnableOtelDebugMetrics(false).build();
+ assertThat(disabled.isEnableOtelMetrics()).isFalse();
+ assertThat(disabled.isEnableOtelDebugMetrics()).isFalse();
+ });
+
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "false");
+
+ forEachTransport(
+ builder -> {
+ StorageOptions enabled =
+ builder.setEnableOtelMetrics(true).setEnableOtelDebugMetrics(true).build();
+ assertThat(enabled.isEnableOtelMetrics()).isTrue();
+ assertThat(enabled.isEnableOtelDebugMetrics()).isTrue();
+ });
+ }
+
+ @Test
+ public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() throws Exception {
+ Map env = new HashMap<>();
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ forEachTransport(
+ builder -> {
+ StorageOptions enabled =
+ builder.setEnableOtelMetrics(true).setEnableOtelDebugMetrics(true).build();
+ assertThat(enabled.isEnableOtelMetrics()).isTrue();
+ assertThat(enabled.isEnableOtelDebugMetrics()).isTrue();
+ });
+
+ forEachTransport(
+ builder -> {
+ StorageOptions disabled =
+ builder.setEnableOtelMetrics(false).setEnableOtelDebugMetrics(false).build();
+ assertThat(disabled.isEnableOtelMetrics()).isFalse();
+ assertThat(disabled.isEnableOtelDebugMetrics()).isFalse();
+ });
+ }
+
+ @Test
+ public void debugMetricsGatedOnBaseMetrics_builder() throws Exception {
+ forEachTransport(
+ builder -> {
+ StorageOptions debugOnly =
+ builder.setEnableOtelMetrics(false).setEnableOtelDebugMetrics(true).build();
+ assertThat(debugOnly.isEnableOtelMetrics()).isFalse();
+ assertThat(debugOnly.isEnableOtelDebugMetrics()).isFalse();
+ });
+
+ forEachTransport(
+ builder -> {
+ StorageOptions both =
+ builder.setEnableOtelMetrics(true).setEnableOtelDebugMetrics(true).build();
+ assertThat(both.isEnableOtelMetrics()).isTrue();
+ assertThat(both.isEnableOtelDebugMetrics()).isTrue();
+ });
+ }
+
+ @Test
+ public void debugMetricsGatedOnBaseMetrics_env() throws Exception {
+ Map env = new HashMap<>();
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ // Base metrics unset (defaults to false) -> debug metrics should evaluate to false
+ forEachTransport(
+ builder -> {
+ StorageOptions envDebugOnly = builder.build();
+ assertThat(envDebugOnly.isEnableOtelMetrics()).isFalse();
+ assertThat(envDebugOnly.isEnableOtelDebugMetrics()).isFalse();
+ });
+
+ // Base metrics explicitly false in env -> debug metrics should evaluate to false
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false");
+ forEachTransport(
+ builder -> {
+ StorageOptions envExplicitFalse = builder.build();
+ assertThat(envExplicitFalse.isEnableOtelMetrics()).isFalse();
+ assertThat(envExplicitFalse.isEnableOtelDebugMetrics()).isFalse();
+ });
+
+ // Base metrics enabled in env -> debug metrics should evaluate to true
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ forEachTransport(
+ builder -> {
+ StorageOptions envBoth = builder.build();
+ assertThat(envBoth.isEnableOtelMetrics()).isTrue();
+ assertThat(envBoth.isEnableOtelDebugMetrics()).isTrue();
+ });
+ }
+
+ @Test
+ public void permissiveBooleanParsing_trueValues() {
+ String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "};
+ for (String val : trueValues) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(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
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, null))
+ .isTrue();
+ // When builderSetting is provided, builder takes precedence over env var
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, false))
+ .isFalse();
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, true))
+ .isTrue();
+ }
+ }
+
+ @Test
+ public void permissiveBooleanParsing_falseValues() {
+ String[] falseValues = {"0", "f", "F", "false", "FALSE", "False", " false "};
+ for (String val : falseValues) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(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
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, null))
+ .isFalse();
+ // When builderSetting is provided, builder takes precedence over env var
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, true))
+ .isTrue();
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, false))
+ .isFalse();
+ }
+ }
+
+ @Test
+ public void permissiveBooleanParsing_invalidValuesFallback() {
+ String[] invalidValues = {"invalid", "2", "yes", "no", "enabled", ""};
+ for (String val : invalidValues) {
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(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
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, true))
+ .isTrue();
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, false))
+ .isFalse();
+ // When builder setting is null, falls back to default false
+ assertWithMessage("Failed for input value: '%s'", val)
+ .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, null))
+ .isFalse();
+ }
+ }
+
+ @Test
+ public void permissiveBooleanParsing_nullHandled() {
+ Map emptyEnv = new HashMap<>();
+ assertThat(StorageMetricsConfig.isEnableOtelMetrics(emptyEnv::get, null)).isFalse();
+ assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull();
+ }
+
+ @Test
+ public void toBuilder_doesNotBakeInEnvironmentVariable() throws Exception {
+ Map env = new HashMap<>();
+
+ forEachTransport(
+ builder -> {
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true");
+ env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true");
+ StorageMetricsConfig.setEnvResolverForTesting(env::get);
+
+ StorageOptions options = builder.build();
+ assertThat(options.isEnableOtelMetrics()).isTrue();
+ assertThat(options.isEnableOtelDebugMetrics()).isTrue();
+
+ // Clear environment variable and rebuild
+ env.clear();
+ StorageOptions rebuilt = options.toBuilder().build();
+ assertThat(rebuilt.isEnableOtelMetrics()).isFalse();
+ assertThat(rebuilt.isEnableOtelDebugMetrics()).isFalse();
+ });
+ }
+
+ @Test
+ public void environmentVariableJava() throws Exception {
+ 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);
+
+ forEachTransport(
+ builder -> {
+ StorageOptions options = builder.build();
+ assertThat(options.isEnableOtelMetrics()).isTrue();
+ assertThat(options.isEnableOtelDebugMetrics()).isTrue();
+ });
+ }
+
+ @Test
+ public void setMetricInterval_rejectsNullZeroOrNegative() throws Exception {
+ forEachTransport(
+ builder -> {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMetricInterval(Duration.ofSeconds(-1)));
+ assertThrows(
+ IllegalArgumentException.class, () -> builder.setMetricInterval(Duration.ZERO));
+ assertThrows(IllegalArgumentException.class, () -> builder.setMetricInterval(null));
+ });
+ }
+
+ @Test
+ public void setMetricInterval_rejectsSubSixtySecondsWithoutCustomProvider() throws Exception {
+ // Reject intervals < 60s when meterProvider == null (SDK-owned Cloud Monitoring exporter)
+ forEachTransport(
+ builder -> {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMetricInterval(Duration.ofSeconds(59)).build());
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> builder.setMetricInterval(Duration.ofSeconds(30)).build());
+ });
+ }
+
+ @Test
+ public void setMetricInterval_acceptsSixtySecondsOrGreaterWithDefaultProvider() throws Exception {
+ forEachTransport(
+ builder -> {
+ StorageOptions options = builder.setMetricInterval(Duration.ofSeconds(60)).build();
+ assertThat(options.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ });
+ }
+
+ @Test
+ public void setMetricInterval_acceptsSubSixtySecondsWithCustomMeterProvider() throws Exception {
+ MeterProvider customMeterProvider = SdkMeterProvider.builder().build();
+
+ forEachTransport(
+ builder -> {
+ StorageOptions customOptions =
+ builder
+ .setMeterProvider(customMeterProvider)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(customOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+ });
+ }
+
+ @Test
+ public void setMetricInterval_acceptsSubSixtySecondsWithCustomOpenTelemetry() throws Exception {
+ io.opentelemetry.api.OpenTelemetry customOtel = io.opentelemetry.api.OpenTelemetry.noop();
+
+ forEachTransport(
+ builder -> {
+ StorageOptions customOptionsOtel =
+ builder
+ .setOpenTelemetry(customOtel)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(customOptionsOtel.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+ });
+ }
+
+ @Test
+ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws Exception {
+ MeterProvider customMeterProvider = SdkMeterProvider.builder().build();
+
+ forEachTransport(
+ builder -> {
+ StorageOptions options =
+ builder.setEnableOtelMetrics(true).setMeterProvider(customMeterProvider).build();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(options);
+ }
+
+ StorageOptions deserialized;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserialized = (StorageOptions) ois.readObject();
+ }
+
+ assertThat(deserialized).isNotNull();
+ assertThat(deserialized.isEnableOtelMetrics()).isTrue();
+ // Transient meterProvider does not carry over; returns null
+ assertThat(deserialized.getMeterProvider()).isNull();
+ });
+ }
+
+ @Test
+ public void serializationRoundTrip_customMeterProviderWithSub60sInterval_clampsToSixtySeconds()
+ throws Exception {
+ MeterProvider customMeterProvider = SdkMeterProvider.builder().build();
+
+ forEachTransport(
+ builder -> {
+ StorageOptions options =
+ builder
+ .setMeterProvider(customMeterProvider)
+ .setMetricInterval(Duration.ofSeconds(30))
+ .build();
+ assertThat(options.getMetricInterval()).isEqualTo(Duration.ofSeconds(30));
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(options);
+ }
+
+ StorageOptions deserialized;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserialized = (StorageOptions) ois.readObject();
+ }
+
+ assertThat(deserialized).isNotNull();
+ assertThat(deserialized.getMeterProvider()).isNull();
+ assertThat(deserialized.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ });
+ }
+
+ @Test
+ public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throws Exception {
+ Field builderField = StorageOptions.Builder.class.getDeclaredField("metricInterval");
+ builderField.setAccessible(true);
+
+ forEachTransport(
+ builder -> {
+ StorageOptions defaultOptions = builder.build();
+ builderField.set(builder, null);
+ StorageOptions optionsWithNullInterval = builder.build();
+
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
+ oos.writeObject(optionsWithNullInterval);
+ }
+
+ StorageOptions deserialized;
+ try (ObjectInputStream ois =
+ new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
+ deserialized = (StorageOptions) ois.readObject();
+ }
+
+ assertThat(deserialized).isNotNull();
+ // Verify NPE guard defaults to 60 seconds
+ assertThat(deserialized.getMetricInterval()).isEqualTo(Duration.ofSeconds(60));
+ // Verify equals and hashCode match a default instance
+ assertThat(deserialized).isEqualTo(defaultOptions);
+ assertThat(defaultOptions).isEqualTo(deserialized);
+ assertThat(deserialized.hashCode()).isEqualTo(defaultOptions.hashCode());
+ });
+ }
+}