From 07f72bed7bec9e66355683e05866e3dab2d67132 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 16 Sep 2026 09:21:37 +0000 Subject: [PATCH 01/28] feat(storage): add OpenTelemetry client metrics infrastructure and configuration - Add configuration options to StorageOptions, GrpcStorageOptions, and HttpStorageOptions (enableOtelMetrics, enableOtelDebugMetrics, meterProvider, metricInterval). - Add development gate checking system properties and environment variables in StorageMetricsConfig. - Implement package-private instrument registry in StorageClientMetrics. - Update OpenTelemetryBootstrappingUtils to register client views with custom histogram boundaries and create client meter provider. - Add comprehensive unit tests in StorageOptionsTest, StorageClientMetricsTest, and OpenTelemetryBootstrappingUtilsTest. --- java-storage/google-cloud-storage/pom.xml | 5 + .../cloud/storage/GrpcStorageOptions.java | 120 ++++++++ .../cloud/storage/HttpStorageOptions.java | 126 +++++++- .../OpenTelemetryBootstrappingUtils.java | 121 ++++++-- .../cloud/storage/StorageClientMetrics.java | 273 ++++++++++++++++++ .../cloud/storage/StorageMetricsConfig.java | 88 ++++++ .../google/cloud/storage/StorageOptions.java | 72 +++++ .../OpenTelemetryBootstrappingUtilsTest.java | 25 ++ .../storage/StorageClientMetricsTest.java | 194 +++++++++++++ .../cloud/storage/StorageOptionsTest.java | 164 +++++++++++ 10 files changed, 1170 insertions(+), 18 deletions(-) create mode 100644 java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java create mode 100644 java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java create mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java create mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java diff --git a/java-storage/google-cloud-storage/pom.xml b/java-storage/google-cloud-storage/pom.xml index 3fadd4c12810..3e7ae71ea283 100644 --- a/java-storage/google-cloud-storage/pom.xml +++ b/java-storage/google-cloud-storage/pom.xml @@ -241,6 +241,11 @@ opentelemetry-sdk-trace test + + io.opentelemetry + opentelemetry-sdk-testing + test + io.grpc 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..22ef4e0138f2 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.sdk.metrics.SdkMeterProvider; 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 @@ -148,6 +150,10 @@ public final class GrpcStorageOptions extends StorageOptions private final GrpcInterceptorProvider grpcInterceptorProvider; private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; + private final boolean enableOtelMetrics; + private final boolean enableOtelDebugMetrics; + private final transient SdkMeterProvider meterProvider; + private final java.time.Duration metricInterval; private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -165,6 +171,16 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.grpcInterceptorProvider = builder.grpcInterceptorProvider; this.blobWriteSessionConfig = builder.blobWriteSessionConfig; this.openTelemetry = builder.openTelemetry; + this.enableOtelMetrics = + builder.enableOtelMetrics != null + ? builder.enableOtelMetrics + : StorageMetricsConfig.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = + builder.enableOtelDebugMetrics != null + ? builder.enableOtelDebugMetrics + : StorageMetricsConfig.isEnableOtelDebugMetrics(); + this.meterProvider = builder.meterProvider; + this.metricInterval = builder.metricInterval; } @Override @@ -414,6 +430,42 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelMetrics() { + return enableOtelMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelDebugMetrics() { + return enableOtelDebugMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public @Nullable SdkMeterProvider getMeterProvider() { + return meterProvider; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public java.time.Duration getMetricInterval() { + return metricInterval; + } + /** * @since 2.14.0 */ @@ -432,6 +484,10 @@ public int hashCode() { grpcInterceptorProvider, blobWriteSessionConfig, openTelemetry, + enableOtelMetrics, + enableOtelDebugMetrics, + meterProvider, + metricInterval, baseHashCode()); } @@ -446,11 +502,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(meterProvider, that.meterProvider) + && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -501,6 +561,10 @@ public static final class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = GrpcStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); + private Boolean enableOtelMetrics = null; + private Boolean enableOtelDebugMetrics = null; + private SdkMeterProvider meterProvider = null; + private java.time.Duration metricInterval = java.time.Duration.ofSeconds(60); private boolean grpcMetricsManuallyEnabled = false; @@ -516,6 +580,10 @@ public static final class Builder extends StorageOptions.Builder { this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; this.openTelemetry = gso.openTelemetry; + this.enableOtelMetrics = gso.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = gso.isEnableOtelDebugMetrics(); + this.meterProvider = gso.getMeterProvider(); + this.metricInterval = gso.getMetricInterval(); } /** @@ -750,6 +818,58 @@ public GrpcStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) return this; } + /** + * Enable or disable OpenTelemetry client metrics. + * + * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { + this.enableOtelDebugMetrics = enableOtelDebugMetrics; + return this; + } + + /** + * Set a custom {@link SdkMeterProvider} for recording client metrics. + * + * @param meterProvider custom SdkMeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider) { + this.meterProvider = meterProvider; + return this; + } + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setMetricInterval(java.time.Duration metricInterval) { + this.metricInterval = requireNonNull(metricInterval, "metricInterval must be non null"); + return this; + } + /** * @since 2.14.0 */ 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..e276fc8b5497 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.sdk.metrics.SdkMeterProvider; 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 @@ -66,6 +69,10 @@ public class HttpStorageOptions extends StorageOptions { private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; + private final boolean enableOtelMetrics; + private final boolean enableOtelDebugMetrics; + private final transient SdkMeterProvider meterProvider; + private final Duration metricInterval; private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -76,6 +83,16 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { retryDepsAdapter = new RetryDependenciesAdapter(); blobWriteSessionConfig = builder.blobWriteSessionConfig; openTelemetry = builder.openTelemetry; + this.enableOtelMetrics = + builder.enableOtelMetrics != null + ? builder.enableOtelMetrics + : StorageMetricsConfig.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = + builder.enableOtelDebugMetrics != null + ? builder.enableOtelDebugMetrics + : StorageMetricsConfig.isEnableOtelDebugMetrics(); + this.meterProvider = builder.meterProvider; + this.metricInterval = builder.metricInterval; } @Override @@ -102,6 +119,42 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelMetrics() { + return enableOtelMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelDebugMetrics() { + return enableOtelDebugMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public @Nullable SdkMeterProvider getMeterProvider() { + return meterProvider; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public Duration getMetricInterval() { + return metricInterval; + } + @Override public HttpStorageOptions.Builder toBuilder() { return new HttpStorageOptions.Builder(this); @@ -110,7 +163,14 @@ public HttpStorageOptions.Builder toBuilder() { @Override public int hashCode() { return Objects.hash( - retryAlgorithmManager, blobWriteSessionConfig, openTelemetry, baseHashCode()); + retryAlgorithmManager, + blobWriteSessionConfig, + openTelemetry, + enableOtelMetrics, + enableOtelDebugMetrics, + meterProvider, + metricInterval, + baseHashCode()); } @Override @@ -125,6 +185,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(meterProvider, that.meterProvider) + && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -157,6 +221,10 @@ public static class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = HttpStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = HttpStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); + private Boolean enableOtelMetrics = null; + private Boolean enableOtelDebugMetrics = null; + private SdkMeterProvider meterProvider = null; + private Duration metricInterval = Duration.ofSeconds(60); Builder() {} @@ -166,6 +234,10 @@ public static class Builder extends StorageOptions.Builder { this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; this.blobWriteSessionConfig = hso.blobWriteSessionConfig; this.openTelemetry = hso.getOpenTelemetry(); + this.enableOtelMetrics = hso.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = hso.isEnableOtelDebugMetrics(); + this.meterProvider = hso.getMeterProvider(); + this.metricInterval = hso.getMetricInterval(); } @Override @@ -317,6 +389,58 @@ public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) this.openTelemetry = openTelemetry; return this; } + + /** + * Enable or disable OpenTelemetry client metrics. + * + * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { + this.enableOtelDebugMetrics = enableOtelDebugMetrics; + return this; + } + + /** + * Set a custom {@link SdkMeterProvider} for recording client metrics. + * + * @param meterProvider custom SdkMeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider) { + this.meterProvider = meterProvider; + return this; + } + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setMetricInterval(Duration metricInterval) { + this.metricInterval = requireNonNull(metricInterval, "metricInterval must be non null"); + return this; + } } public static final class HttpStorageDefaults extends StorageDefaults { diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 53fea0b8b6c2..be802cc4769e 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -205,12 +205,62 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) return metricServiceEndpoint + ":" + endpoint.split(":")[1]; } + static final ImmutableList CLIENT_LATENCY_HISTOGRAMS = + ImmutableList.of( + StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, + StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, + StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION); + + static final ImmutableList CLIENT_SIZE_HISTOGRAMS = + ImmutableList.of( + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE); + @VisibleForTesting static SdkMeterProvider createMeterProvider( String metricServiceEndpoint, String projectIdToUse, Attributes detectedAttributes, boolean shouldSuppressExceptions) { + return createClientMeterProvider( + metricServiceEndpoint, + projectIdToUse, + detectedAttributes, + java.time.Duration.ofSeconds(60), + shouldSuppressExceptions, + "grpc"); + } + + @VisibleForTesting + static SdkMeterProvider createClientMeterProvider( + String metricServiceEndpoint, + String projectIdToUse, + Attributes detectedAttributes, + java.time.Duration metricInterval, + boolean shouldSuppressExceptions) { + return createClientMeterProvider( + metricServiceEndpoint, + projectIdToUse, + detectedAttributes, + metricInterval, + shouldSuppressExceptions, + "storage"); + } + + @VisibleForTesting + static SdkMeterProvider createClientMeterProvider( + String metricServiceEndpoint, + String projectIdToUse, + Attributes detectedAttributes, + java.time.Duration metricInterval, + boolean shouldSuppressExceptions, + String api) { MonitoredResourceDescription monitoredResourceDescription = new MonitoredResourceDescription( @@ -239,6 +289,22 @@ static SdkMeterProvider createMeterProvider( InstrumentSelector.builder().setName(metric).build(), View.builder().setName(metric.replace(".", "/")).build()); } + addHistogramView( + providerBuilder, latencyHistogramBoundaries(), "grpc/client/attempt/duration", "s"); + addHistogramView( + providerBuilder, + sizeHistogramBoundaries(), + "grpc/client/attempt/rcvd_total_compressed_message_size", + "By"); + addHistogramView( + providerBuilder, + sizeHistogramBoundaries(), + "grpc/client/attempt/sent_total_compressed_message_size", + "By"); + + // Register views for client histograms + registerClientViews(providerBuilder); + MetricExporter exporter = shouldSuppressExceptions ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter) @@ -248,7 +314,7 @@ static SdkMeterProvider createMeterProvider( .put("gcp.resource_type", "storage.googleapis.com/Client") .put("project_id", projectIdToUse) .put("instance_id", UUID.randomUUID().toString()) - .put("api", "grpc"); + .put("api", api != null ? api : "storage"); String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); if (detectedLocation != null) { attributesBuilder.put("location", detectedLocation); @@ -270,26 +336,45 @@ static SdkMeterProvider createMeterProvider( providerBuilder .registerMetricReader( PeriodicMetricReader.builder(exporter) - .setInterval(java.time.Duration.ofSeconds(60)) + .setInterval( + metricInterval != null ? metricInterval : java.time.Duration.ofSeconds(60)) .build()) .setResource(Resource.create(attributesBuilder.build())); - addHistogramView( - providerBuilder, latencyHistogramBoundaries(), "grpc/client/attempt/duration", "s"); - addHistogramView( - providerBuilder, - sizeHistogramBoundaries(), - "grpc/client/attempt/rcvd_total_compressed_message_size", - "By"); - addHistogramView( - providerBuilder, - sizeHistogramBoundaries(), - "grpc/client/attempt/sent_total_compressed_message_size", - "By"); - return providerBuilder.build(); } + @VisibleForTesting + static SdkMeterProviderBuilder registerClientViews(SdkMeterProviderBuilder providerBuilder) { + for (String metric : CLIENT_LATENCY_HISTOGRAMS) { + addClientHistogramView(providerBuilder, latencyHistogramBoundaries(), metric, "s"); + } + for (String metric : CLIENT_SIZE_HISTOGRAMS) { + addClientHistogramView(providerBuilder, sizeHistogramBoundaries(), metric, "By"); + } + return providerBuilder; + } + + private static void addClientHistogramView( + SdkMeterProviderBuilder provider, List boundaries, String name, String unit) { + InstrumentSelector instrumentSelector = + InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setUnit(unit) + .setName(name) + .build(); + View view = + View.builder() + .setName(name) + .setDescription( + "A view of " + + name + + " with histogram boundaries more appropriate for Google Cloud Storage RPCs") + .setAggregation(Aggregation.explicitBucketHistogram(boundaries)) + .build(); + provider.registerView(instrumentSelector, view); + } + private static void addHistogramView( SdkMeterProviderBuilder provider, List boundaries, String name, String unit) { InstrumentSelector instrumentSelector = @@ -312,7 +397,8 @@ private static void addHistogramView( provider.registerView(instrumentSelector, view); } - private static List latencyHistogramBoundaries() { + @VisibleForTesting + static List latencyHistogramBoundaries() { List boundaries = new ArrayList<>(); BigDecimal boundary = new BigDecimal(0, MathContext.UNLIMITED); BigDecimal increment = new BigDecimal("0.002", MathContext.UNLIMITED); // 2ms @@ -337,7 +423,8 @@ private static List latencyHistogramBoundaries() { return boundaries; } - private static List sizeHistogramBoundaries() { + @VisibleForTesting + static List sizeHistogramBoundaries() { long kb = 1024; long mb = 1024 * kb; long gb = 1024 * mb; diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java new file mode 100644 index 000000000000..f94eaa6448c2 --- /dev/null +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -0,0 +1,273 @@ +/* + * 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 io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** Package-private instrument registry for OpenTelemetry client metrics. */ +final class StorageClientMetrics { + static final String METER_NAME = "com.google.cloud.storage"; + + // Standard metric names + static final String METRIC_RPC_CLIENT_CALL_DURATION = "rpc.client.call.duration"; + static final String METRIC_HTTP_CLIENT_REQUEST_DURATION = "http.client.request.duration"; + static final String METRIC_GCP_CLIENT_REQUEST_DURATION = "gcp.client.request.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_OPERATIONS = "gcp.storage.client.operations"; + static final String METRIC_GCP_STORAGE_CLIENT_ATTEMPTS = "gcp.storage.client.attempts"; + static final String METRIC_GCP_STORAGE_CLIENT_ERRORS = "gcp.storage.client.errors"; + static final String METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB = + "gcp.storage.client.operation.ttfb"; + static final String METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE = + "gcp.storage.client.request.body.size"; + static final String METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE = + "gcp.storage.client.response.body.size"; + + // Debug metric names + static final String METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE = + "gcp.storage.client.request.active"; + static final String METRIC_GCP_STORAGE_CLIENT_GFE_DURATION = "gcp.storage.client.gfe.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING = + "gcp.storage.client.gfe.header_missing"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION = + "gcp.storage.client.network.dns.lookup.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION = + "gcp.storage.client.network.tcp.connect.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION = + "gcp.storage.client.network.tls.handshake.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = + "gcp.storage.client.auth.credential_refresh.duration"; + + private final DoubleHistogram rpcClientCallDuration; + private final DoubleHistogram httpClientRequestDuration; + private final DoubleHistogram gcpClientRequestDuration; + private final LongCounter operations; + private final LongCounter attempts; + private final LongCounter errors; + private final DoubleHistogram operationTtfb; + private final LongHistogram requestBodySize; + private final LongHistogram responseBodySize; + + @Nullable private final LongUpDownCounter requestActive; + @Nullable private final DoubleHistogram gfeDuration; + @Nullable private final LongCounter gfeHeaderMissing; + @Nullable private final DoubleHistogram dnsLookupDuration; + @Nullable private final DoubleHistogram tcpConnectDuration; + @Nullable private final DoubleHistogram tlsHandshakeDuration; + @Nullable private final DoubleHistogram credentialRefreshDuration; + + static StorageClientMetrics create( + MeterProvider meterProvider, boolean enableOtelDebugMetrics) { + Meter meter = + meterProvider + .meterBuilder(METER_NAME) + .setInstrumentationVersion(StorageOptions.version()) + .build(); + return new StorageClientMetrics(meter, enableOtelDebugMetrics); + } + + StorageClientMetrics(Meter meter, boolean enableOtelDebugMetrics) { + this.rpcClientCallDuration = + meter + .histogramBuilder(METRIC_RPC_CLIENT_CALL_DURATION) + .setDescription("Duration of one gRPC request. Retries not included (Otel)") + .setUnit("s") + .build(); + this.httpClientRequestDuration = + meter + .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) + .setDescription("Duration of one HTTP client request. Retried not included (Otel)") + .setUnit("s") + .build(); + this.gcpClientRequestDuration = + meter + .histogramBuilder(METRIC_GCP_CLIENT_REQUEST_DURATION) + .setDescription("Latency of a client operation") + .setUnit("s") + .build(); + this.operations = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATIONS) + .setDescription("Number of GCS client operations") + .setUnit("1") + .build(); + this.attempts = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ATTEMPTS) + .setDescription("Number of GCS client attempts") + .setUnit("1") + .build(); + this.errors = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) + .setDescription("Number of GCS client errors") + .setUnit("1") + .build(); + this.operationTtfb = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB) + .setDescription("Time to first byte of GCS client operations") + .setUnit("s") + .build(); + this.requestBodySize = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE) + .ofLongs() + .setDescription("Size of GCS client request body") + .setUnit("By") + .build(); + this.responseBodySize = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE) + .ofLongs() + .setDescription("Size of GCS client response body") + .setUnit("By") + .build(); + + if (enableOtelDebugMetrics) { + this.requestActive = + meter + .upDownCounterBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE) + .setDescription("Number of active GCS client requests") + .setUnit("1") + .build(); + this.gfeDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_GFE_DURATION) + .setDescription("GFE proxy processing time") + .setUnit("s") + .build(); + this.gfeHeaderMissing = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING) + .setDescription( + "Number of GCS requests where the X-Goog-Gfe-Service-Time header was missing") + .setUnit("1") + .build(); + this.dnsLookupDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION) + .setDescription("Time taken for DNS lookup") + .setUnit("s") + .build(); + this.tcpConnectDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION) + .setDescription("Time taken for TCP connection") + .setUnit("s") + .build(); + this.tlsHandshakeDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION) + .setDescription("Time taken to perform a TLS handshake") + .setUnit("s") + .build(); + this.credentialRefreshDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION) + .setDescription( + "Duration of the background API/network calls made to refresh OAuth2/JWT access" + + " credentials.") + .setUnit("s") + .build(); + } else { + this.requestActive = null; + this.gfeDuration = null; + this.gfeHeaderMissing = null; + this.dnsLookupDuration = null; + this.tcpConnectDuration = null; + this.tlsHandshakeDuration = null; + this.credentialRefreshDuration = null; + } + } + + DoubleHistogram getRpcClientCallDuration() { + return rpcClientCallDuration; + } + + DoubleHistogram getHttpClientRequestDuration() { + return httpClientRequestDuration; + } + + DoubleHistogram getGcpClientRequestDuration() { + return gcpClientRequestDuration; + } + + LongCounter getOperations() { + return operations; + } + + LongCounter getAttempts() { + return attempts; + } + + LongCounter getErrors() { + return errors; + } + + DoubleHistogram getOperationTtfb() { + return operationTtfb; + } + + LongHistogram getRequestBodySize() { + return requestBodySize; + } + + LongHistogram getResponseBodySize() { + return responseBodySize; + } + + @Nullable + LongUpDownCounter getRequestActive() { + return requestActive; + } + + @Nullable + DoubleHistogram getGfeDuration() { + return gfeDuration; + } + + @Nullable + LongCounter getGfeHeaderMissing() { + return gfeHeaderMissing; + } + + @Nullable + DoubleHistogram getDnsLookupDuration() { + return dnsLookupDuration; + } + + @Nullable + DoubleHistogram getTcpConnectDuration() { + return tcpConnectDuration; + } + + @Nullable + DoubleHistogram getTlsHandshakeDuration() { + return tlsHandshakeDuration; + } + + @Nullable + DoubleHistogram getCredentialRefreshDuration() { + return credentialRefreshDuration; + } +} 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..50459340068c --- /dev/null +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java @@ -0,0 +1,88 @@ +/* + * 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.function.Function; + +final class StorageMetricsConfig { + static final String SYS_PROP_ENABLE_OTEL_METRICS = "com.google.cloud.storage.enable_otel_metrics"; + static final String SYS_PROP_ENABLE_OTEL_DEBUG_METRICS = + "com.google.cloud.storage.enable_otel_debug_metrics"; + static final String ENV_ENABLE_OTEL_METRICS_JAVA = "GCP_STORAGE_JAVA_ENABLE_OTEL_METRICS"; + static final String ENV_ENABLE_OTEL_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; + static final String ENV_ENABLE_OTEL_DEBUG_METRICS = + "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; + + private static Function sysPropResolver = System::getProperty; + private static Function envResolver = System::getenv; + + private StorageMetricsConfig() {} + + static boolean isEnableOtelMetrics() { + return isEnableOtelMetrics(sysPropResolver, envResolver); + } + + static boolean isEnableOtelDebugMetrics() { + return isEnableOtelDebugMetrics(sysPropResolver, envResolver); + } + + @VisibleForTesting + static boolean isEnableOtelMetrics( + Function sysProps, Function env) { + String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_METRICS); + if (sysProp != null) { + return Boolean.parseBoolean(sysProp); + } + String javaEnv = env.apply(ENV_ENABLE_OTEL_METRICS_JAVA); + if (javaEnv != null) { + return Boolean.parseBoolean(javaEnv); + } + String fallbackEnv = env.apply(ENV_ENABLE_OTEL_METRICS_FALLBACK); + if (fallbackEnv != null) { + return Boolean.parseBoolean(fallbackEnv); + } + return false; + } + + @VisibleForTesting + static boolean isEnableOtelDebugMetrics( + Function sysProps, Function env) { + String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); + if (sysProp != null) { + return Boolean.parseBoolean(sysProp); + } + String javaEnv = env.apply(ENV_ENABLE_OTEL_DEBUG_METRICS); + if (javaEnv != null) { + return Boolean.parseBoolean(javaEnv); + } + return false; + } + + @VisibleForTesting + static void setResolversForTesting( + Function testSysProps, Function testEnv) { + sysPropResolver = testSysProps != null ? testSysProps : System::getProperty; + envResolver = testEnv != null ? testEnv : System::getenv; + } + + @VisibleForTesting + static void resetResolversForTesting() { + sysPropResolver = System::getProperty; + 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..e6647ef80555 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 @@ -33,11 +33,14 @@ import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.spi.StorageRpcFactory; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; import java.io.IOException; import java.io.InputStream; +import java.time.Duration; import java.util.Locale; import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public abstract class StorageOptions extends ServiceOptions { @@ -143,6 +146,43 @@ 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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics); + + /** + * Enable or disable OpenTelemetry debug client metrics. + * + * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setEnableOtelDebugMetrics( + boolean enableOtelDebugMetrics); + + /** + * Set a custom {@link SdkMeterProvider} for recording client metrics. + * + * @param meterProvider custom SdkMeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider); + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setMetricInterval(Duration metricInterval); + @Override public abstract StorageOptions build(); } @@ -185,6 +225,38 @@ public static String version() { @BetaApi public abstract OpenTelemetry getOpenTelemetry(); + /** + * Whether OpenTelemetry client metrics are enabled. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract boolean isEnableOtelMetrics(); + + /** + * Whether OpenTelemetry debug client metrics are enabled. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract boolean isEnableOtelDebugMetrics(); + + /** + * The {@link SdkMeterProvider} configured for recording client metrics. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract @Nullable SdkMeterProvider getMeterProvider(); + + /** + * The metric export interval configured for periodic metric reading. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract Duration getMetricInterval(); + @SuppressWarnings("unchecked") @Override public abstract StorageOptions.Builder toBuilder(); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index 5e93c95a988b..73799e05dac3 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -22,6 +22,7 @@ import com.google.cloud.storage.OpenTelemetryBootstrappingUtils.ChannelConfigurator; import io.grpc.ManagedChannelBuilder; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; @@ -94,4 +95,28 @@ public void channelConfigurator_andThen_nullsafe() { ChannelConfigurator actual = ChannelConfigurator.identity().andThen(null); assertThat(actual).isSameInstanceAs(ChannelConfigurator.identity()); } + + @Test + public void histogramBoundaries_areValidAndIncreasing() { + List latencyBoundaries = OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries(); + assertThat(latencyBoundaries).isNotEmpty(); + for (int i = 1; i < latencyBoundaries.size(); i++) { + assertThat(latencyBoundaries.get(i)).isGreaterThan(latencyBoundaries.get(i - 1)); + } + + List sizeBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); + assertThat(sizeBoundaries).isNotEmpty(); + for (int i = 1; i < sizeBoundaries.size(); i++) { + assertThat(sizeBoundaries.get(i)).isGreaterThan(sizeBoundaries.get(i - 1)); + } + } + + @Test + public void registerClientViews_doesNotThrow() { + io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder builder = + io.opentelemetry.sdk.metrics.SdkMeterProvider.builder(); + io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder returned = + OpenTelemetryBootstrappingUtils.registerClientViews(builder); + assertThat(returned).isSameInstanceAs(builder); + } } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java new file mode 100644 index 000000000000..3b1b6ff6f61e --- /dev/null +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -0,0 +1,194 @@ +/* + * 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 io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; +import io.opentelemetry.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.Test; + +public final class StorageClientMetricsTest { + + @Test + public void standardMetrics_initializedWithoutDebug() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, false); + + assertThat(metrics.getRpcClientCallDuration()).isNotNull(); + assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); + assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); + assertThat(metrics.getOperations()).isNotNull(); + assertThat(metrics.getAttempts()).isNotNull(); + assertThat(metrics.getErrors()).isNotNull(); + assertThat(metrics.getOperationTtfb()).isNotNull(); + assertThat(metrics.getRequestBodySize()).isNotNull(); + assertThat(metrics.getResponseBodySize()).isNotNull(); + + // Debug instruments should be null + assertThat(metrics.getRequestActive()).isNull(); + assertThat(metrics.getGfeDuration()).isNull(); + assertThat(metrics.getGfeHeaderMissing()).isNull(); + assertThat(metrics.getDnsLookupDuration()).isNull(); + assertThat(metrics.getTcpConnectDuration()).isNull(); + assertThat(metrics.getTlsHandshakeDuration()).isNull(); + assertThat(metrics.getCredentialRefreshDuration()).isNull(); + } + + @Test + public void debugMetrics_initializedWhenEnabled() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true); + + assertThat(metrics.getRpcClientCallDuration()).isNotNull(); + assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); + assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); + assertThat(metrics.getOperations()).isNotNull(); + assertThat(metrics.getAttempts()).isNotNull(); + assertThat(metrics.getErrors()).isNotNull(); + assertThat(metrics.getOperationTtfb()).isNotNull(); + assertThat(metrics.getRequestBodySize()).isNotNull(); + assertThat(metrics.getResponseBodySize()).isNotNull(); + + // Debug instruments should be present + assertThat(metrics.getRequestActive()).isNotNull(); + assertThat(metrics.getGfeDuration()).isNotNull(); + assertThat(metrics.getGfeHeaderMissing()).isNotNull(); + assertThat(metrics.getDnsLookupDuration()).isNotNull(); + assertThat(metrics.getTcpConnectDuration()).isNotNull(); + assertThat(metrics.getTlsHandshakeDuration()).isNotNull(); + assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); + } + + @Test + public void inMemoryMetricReader_verifiesViewsAndBoundaries() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProviderBuilder providerBuilder = + SdkMeterProvider.builder().registerMetricReader(reader); + OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); + SdkMeterProvider provider = providerBuilder.build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true); + + // Record data for each instrument + metrics.getRpcClientCallDuration().record(0.123); + metrics.getHttpClientRequestDuration().record(0.234); + metrics.getGcpClientRequestDuration().record(0.345); + metrics.getOperationTtfb().record(0.045); + metrics.getGfeDuration().record(0.012); + metrics.getDnsLookupDuration().record(0.005); + metrics.getTcpConnectDuration().record(0.015); + metrics.getTlsHandshakeDuration().record(0.025); + metrics.getCredentialRefreshDuration().record(0.080); + + metrics.getRequestBodySize().record(1024 * 512); + metrics.getResponseBodySize().record(1024 * 1024); + + metrics.getOperations().add(1); + metrics.getAttempts().add(2); + metrics.getErrors().add(1); + metrics.getRequestActive().add(1); + metrics.getGfeHeaderMissing().add(1); + + Collection collectedMetrics = reader.collectAllMetrics(); + Map metricsMap = + collectedMetrics.stream() + .collect(Collectors.toMap(MetricData::getName, Function.identity())); + + List expectedLatencyBoundaries = + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries(); + List expectedSizeBoundaries = + OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); + + // Verify standard latency histograms have custom latency boundaries + assertHistogramBoundaries( + metricsMap, StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, + expectedLatencyBoundaries); + + // Verify debug latency histograms have custom latency boundaries + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION, + expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION, + expectedLatencyBoundaries); + + // Verify size histograms have custom size boundaries + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, + expectedSizeBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, + expectedSizeBoundaries); + + // Verify counter metrics + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS); + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS); + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS); + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING); + } + + private static void assertHistogramBoundaries( + Map metricsMap, String metricName, List expectedBoundaries) { + assertThat(metricsMap).containsKey(metricName); + MetricData metricData = metricsMap.get(metricName); + HistogramPointData pointData = + metricData.getHistogramData().getPoints().iterator().next(); + assertThat(pointData.getBoundaries()).isEqualTo(expectedBoundaries); + } +} 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..f662c30dcd1b --- /dev/null +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java @@ -0,0 +1,164 @@ +/* + * 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 io.opentelemetry.sdk.metrics.SdkMeterProvider; +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() { + System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS); + System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); + 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() { + SdkMeterProvider 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 builder_explicitDisablingOverridesGate() { + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + + HttpStorageOptions httpOptions = + HttpStorageOptions.http().setEnableOtelMetrics(false).build(); + assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + + GrpcStorageOptions grpcOptions = + GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + } + + @Test + public void developmentGate_systemProperty() { + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS, "true"); + + 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 developmentGate_environmentVariableJava() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true"); + StorageMetricsConfig.setResolversForTesting(k -> null, 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 developmentGate_environmentVariableFallback() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_FALLBACK, "true"); + StorageMetricsConfig.setResolversForTesting(k -> null, env::get); + + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isTrue(); + } + + @Test + public void developmentGate_systemPropertyTakesPrecedenceOverEnv() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); + Map sysProps = new HashMap<>(); + sysProps.put(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); + StorageMetricsConfig.setResolversForTesting(sysProps::get, env::get); + + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + } +} From 235a2305c9494c9794bd20af8cb2eefbf1356d93 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 16 Sep 2026 09:46:24 +0000 Subject: [PATCH 02/28] fix(storage): resolve code formatting violations for client metrics Apply spotify fmt-maven-plugin formatting to StorageMetricsConfig, StorageClientMetrics, StorageOptionsTest, and StorageClientMetricsTest. [Generated-by: AI] --- .../cloud/storage/StorageClientMetrics.java | 24 +++++++------------ .../cloud/storage/StorageMetricsConfig.java | 3 +-- .../storage/StorageClientMetricsTest.java | 13 +++++----- .../cloud/storage/StorageOptionsTest.java | 6 ++--- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index f94eaa6448c2..a62aad85a213 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -75,8 +75,7 @@ final class StorageClientMetrics { @Nullable private final DoubleHistogram tlsHandshakeDuration; @Nullable private final DoubleHistogram credentialRefreshDuration; - static StorageClientMetrics create( - MeterProvider meterProvider, boolean enableOtelDebugMetrics) { + static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOtelDebugMetrics) { Meter meter = meterProvider .meterBuilder(METER_NAME) @@ -236,38 +235,31 @@ LongHistogram getResponseBodySize() { return responseBodySize; } - @Nullable - LongUpDownCounter getRequestActive() { + @Nullable LongUpDownCounter getRequestActive() { return requestActive; } - @Nullable - DoubleHistogram getGfeDuration() { + @Nullable DoubleHistogram getGfeDuration() { return gfeDuration; } - @Nullable - LongCounter getGfeHeaderMissing() { + @Nullable LongCounter getGfeHeaderMissing() { return gfeHeaderMissing; } - @Nullable - DoubleHistogram getDnsLookupDuration() { + @Nullable DoubleHistogram getDnsLookupDuration() { return dnsLookupDuration; } - @Nullable - DoubleHistogram getTcpConnectDuration() { + @Nullable DoubleHistogram getTcpConnectDuration() { return tcpConnectDuration; } - @Nullable - DoubleHistogram getTlsHandshakeDuration() { + @Nullable DoubleHistogram getTlsHandshakeDuration() { return tlsHandshakeDuration; } - @Nullable - DoubleHistogram getCredentialRefreshDuration() { + @Nullable DoubleHistogram getCredentialRefreshDuration() { return credentialRefreshDuration; } } 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 index 50459340068c..df1a505a9bf4 100644 --- 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 @@ -25,8 +25,7 @@ final class StorageMetricsConfig { "com.google.cloud.storage.enable_otel_debug_metrics"; static final String ENV_ENABLE_OTEL_METRICS_JAVA = "GCP_STORAGE_JAVA_ENABLE_OTEL_METRICS"; static final String ENV_ENABLE_OTEL_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; - static final String ENV_ENABLE_OTEL_DEBUG_METRICS = - "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; + static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; private static Function sysPropResolver = System::getProperty; private static Function envResolver = System::getenv; diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 3b1b6ff6f61e..be7043b4e425 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -123,12 +123,13 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { List expectedLatencyBoundaries = OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries(); - List expectedSizeBoundaries = - OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); + List expectedSizeBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); // Verify standard latency histograms have custom latency boundaries assertHistogramBoundaries( - metricsMap, StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, expectedLatencyBoundaries); + metricsMap, + StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, + expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, @@ -178,7 +179,8 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS); assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS); assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS); - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING); } @@ -187,8 +189,7 @@ private static void assertHistogramBoundaries( Map metricsMap, String metricName, List expectedBoundaries) { assertThat(metricsMap).containsKey(metricName); MetricData metricData = metricsMap.get(metricName); - HistogramPointData pointData = - metricData.getHistogramData().getPoints().iterator().next(); + HistogramPointData pointData = metricData.getHistogramData().getPoints().iterator().next(); assertThat(pointData.getBoundaries()).isEqualTo(expectedBoundaries); } } 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 index f662c30dcd1b..7cb568b29a5b 100644 --- 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 @@ -95,12 +95,10 @@ public void builder_explicitEnabling() { public void builder_explicitDisablingOverridesGate() { System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); - HttpStorageOptions httpOptions = - HttpStorageOptions.http().setEnableOtelMetrics(false).build(); + HttpStorageOptions httpOptions = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); - GrpcStorageOptions grpcOptions = - GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); } From 590ad9edab964fa7ee19b7d6f9e97b035feb2c35 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 16 Sep 2026 09:54:05 +0000 Subject: [PATCH 03/28] fix(storage): resolve PR review comments on OpenTelemetry metrics Use simple class name Duration instead of fully qualified java.time.Duration in GrpcStorageOptions and OpenTelemetryBootstrappingUtils. [Generated-by: AI] --- .../com/google/cloud/storage/GrpcStorageOptions.java | 8 ++++---- .../cloud/storage/OpenTelemetryBootstrappingUtils.java | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) 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 22ef4e0138f2..80de7d9b2c0a 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 @@ -153,7 +153,7 @@ public final class GrpcStorageOptions extends StorageOptions private final boolean enableOtelMetrics; private final boolean enableOtelDebugMetrics; private final transient SdkMeterProvider meterProvider; - private final java.time.Duration metricInterval; + private final Duration metricInterval; private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -462,7 +462,7 @@ public boolean isEnableOtelDebugMetrics() { */ @BetaApi @Override - public java.time.Duration getMetricInterval() { + public Duration getMetricInterval() { return metricInterval; } @@ -564,7 +564,7 @@ public static final class Builder extends StorageOptions.Builder { private Boolean enableOtelMetrics = null; private Boolean enableOtelDebugMetrics = null; private SdkMeterProvider meterProvider = null; - private java.time.Duration metricInterval = java.time.Duration.ofSeconds(60); + private Duration metricInterval = Duration.ofSeconds(60); private boolean grpcMetricsManuallyEnabled = false; @@ -865,7 +865,7 @@ public GrpcStorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvide */ @BetaApi @Override - public GrpcStorageOptions.Builder setMetricInterval(java.time.Duration metricInterval) { + public GrpcStorageOptions.Builder setMetricInterval(Duration metricInterval) { this.metricInterval = requireNonNull(metricInterval, "metricInterval must be non null"); return this; } diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index be802cc4769e..7ef0f1c87567 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -51,6 +51,7 @@ import java.math.BigDecimal; import java.math.MathContext; import java.net.NoRouteToHostException; +import java.time.Duration; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -232,7 +233,7 @@ static SdkMeterProvider createMeterProvider( metricServiceEndpoint, projectIdToUse, detectedAttributes, - java.time.Duration.ofSeconds(60), + Duration.ofSeconds(60), shouldSuppressExceptions, "grpc"); } @@ -242,7 +243,7 @@ static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, String projectIdToUse, Attributes detectedAttributes, - java.time.Duration metricInterval, + Duration metricInterval, boolean shouldSuppressExceptions) { return createClientMeterProvider( metricServiceEndpoint, @@ -258,7 +259,7 @@ static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, String projectIdToUse, Attributes detectedAttributes, - java.time.Duration metricInterval, + Duration metricInterval, boolean shouldSuppressExceptions, String api) { @@ -337,7 +338,7 @@ static SdkMeterProvider createClientMeterProvider( .registerMetricReader( PeriodicMetricReader.builder(exporter) .setInterval( - metricInterval != null ? metricInterval : java.time.Duration.ofSeconds(60)) + metricInterval != null ? metricInterval : Duration.ofSeconds(60)) .build()) .setResource(Resource.create(attributesBuilder.build())); From 02a77403e712b64fd6f65121662812929bc2264f Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 16 Sep 2026 10:14:47 +0000 Subject: [PATCH 04/28] fix(storage): resolve code formatting violation in OpenTelemetryBootstrappingUtils Apply spotify fmt-maven-plugin formatting to OpenTelemetryBootstrappingUtils. [Generated-by: AI] --- .../google/cloud/storage/OpenTelemetryBootstrappingUtils.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 7ef0f1c87567..34ee219c8310 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -337,8 +337,7 @@ static SdkMeterProvider createClientMeterProvider( providerBuilder .registerMetricReader( PeriodicMetricReader.builder(exporter) - .setInterval( - metricInterval != null ? metricInterval : Duration.ofSeconds(60)) + .setInterval(metricInterval != null ? metricInterval : Duration.ofSeconds(60)) .build()) .setResource(Resource.create(attributesBuilder.build())); From f686723815319e383dca4bfd2efa06e551b3c43d Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 23 Sep 2026 10:27:19 +0000 Subject: [PATCH 05/28] feat(storage): add OpenTelemetry client metrics instrument registry and views [Generated-by: AI] --- .../cloud/storage/GrpcStorageOptions.java | 120 ------------- .../cloud/storage/HttpStorageOptions.java | 126 +------------- .../OpenTelemetryBootstrappingUtils.java | 5 +- .../cloud/storage/StorageClientMetrics.java | 52 +++++- .../cloud/storage/StorageMetricsConfig.java | 87 ---------- .../google/cloud/storage/StorageOptions.java | 72 -------- .../storage/StorageClientMetricsTest.java | 21 +++ .../cloud/storage/StorageOptionsTest.java | 162 ------------------ 8 files changed, 75 insertions(+), 570 deletions(-) delete mode 100644 java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java delete mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java 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 80de7d9b2c0a..1a6726b9c01b 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,7 +100,6 @@ import io.grpc.StatusRuntimeException; import io.grpc.protobuf.ProtoUtils; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; @@ -123,7 +122,6 @@ 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 @@ -150,10 +148,6 @@ public final class GrpcStorageOptions extends StorageOptions private final GrpcInterceptorProvider grpcInterceptorProvider; private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; - private final boolean enableOtelMetrics; - private final boolean enableOtelDebugMetrics; - private final transient SdkMeterProvider meterProvider; - private final Duration metricInterval; private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -171,16 +165,6 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.grpcInterceptorProvider = builder.grpcInterceptorProvider; this.blobWriteSessionConfig = builder.blobWriteSessionConfig; this.openTelemetry = builder.openTelemetry; - this.enableOtelMetrics = - builder.enableOtelMetrics != null - ? builder.enableOtelMetrics - : StorageMetricsConfig.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = - builder.enableOtelDebugMetrics != null - ? builder.enableOtelDebugMetrics - : StorageMetricsConfig.isEnableOtelDebugMetrics(); - this.meterProvider = builder.meterProvider; - this.metricInterval = builder.metricInterval; } @Override @@ -430,42 +414,6 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelMetrics() { - return enableOtelMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelDebugMetrics() { - return enableOtelDebugMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public @Nullable SdkMeterProvider getMeterProvider() { - return meterProvider; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public Duration getMetricInterval() { - return metricInterval; - } - /** * @since 2.14.0 */ @@ -484,10 +432,6 @@ public int hashCode() { grpcInterceptorProvider, blobWriteSessionConfig, openTelemetry, - enableOtelMetrics, - enableOtelDebugMetrics, - meterProvider, - metricInterval, baseHashCode()); } @@ -502,15 +446,11 @@ 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(meterProvider, that.meterProvider) - && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -561,10 +501,6 @@ public static final class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = GrpcStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); - private Boolean enableOtelMetrics = null; - private Boolean enableOtelDebugMetrics = null; - private SdkMeterProvider meterProvider = null; - private Duration metricInterval = Duration.ofSeconds(60); private boolean grpcMetricsManuallyEnabled = false; @@ -580,10 +516,6 @@ public static final class Builder extends StorageOptions.Builder { this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; this.openTelemetry = gso.openTelemetry; - this.enableOtelMetrics = gso.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = gso.isEnableOtelDebugMetrics(); - this.meterProvider = gso.getMeterProvider(); - this.metricInterval = gso.getMetricInterval(); } /** @@ -818,58 +750,6 @@ public GrpcStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) return this; } - /** - * Enable or disable OpenTelemetry client metrics. - * - * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public GrpcStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { - this.enableOtelDebugMetrics = enableOtelDebugMetrics; - return this; - } - - /** - * Set a custom {@link SdkMeterProvider} for recording client metrics. - * - * @param meterProvider custom SdkMeterProvider to use - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public GrpcStorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider) { - this.meterProvider = meterProvider; - return this; - } - - /** - * Set the metric export interval for periodic metric reading. - * - * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public GrpcStorageOptions.Builder setMetricInterval(Duration metricInterval) { - this.metricInterval = requireNonNull(metricInterval, "metricInterval must be non null"); - return this; - } - /** * @since 2.14.0 */ 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 e276fc8b5497..dac8a010cdfa 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,16 +42,13 @@ import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableSet; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; 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 @@ -69,10 +66,6 @@ public class HttpStorageOptions extends StorageOptions { private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; - private final boolean enableOtelMetrics; - private final boolean enableOtelDebugMetrics; - private final transient SdkMeterProvider meterProvider; - private final Duration metricInterval; private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -83,16 +76,6 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { retryDepsAdapter = new RetryDependenciesAdapter(); blobWriteSessionConfig = builder.blobWriteSessionConfig; openTelemetry = builder.openTelemetry; - this.enableOtelMetrics = - builder.enableOtelMetrics != null - ? builder.enableOtelMetrics - : StorageMetricsConfig.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = - builder.enableOtelDebugMetrics != null - ? builder.enableOtelDebugMetrics - : StorageMetricsConfig.isEnableOtelDebugMetrics(); - this.meterProvider = builder.meterProvider; - this.metricInterval = builder.metricInterval; } @Override @@ -119,42 +102,6 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelMetrics() { - return enableOtelMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelDebugMetrics() { - return enableOtelDebugMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public @Nullable SdkMeterProvider getMeterProvider() { - return meterProvider; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public Duration getMetricInterval() { - return metricInterval; - } - @Override public HttpStorageOptions.Builder toBuilder() { return new HttpStorageOptions.Builder(this); @@ -163,14 +110,7 @@ public HttpStorageOptions.Builder toBuilder() { @Override public int hashCode() { return Objects.hash( - retryAlgorithmManager, - blobWriteSessionConfig, - openTelemetry, - enableOtelMetrics, - enableOtelDebugMetrics, - meterProvider, - metricInterval, - baseHashCode()); + retryAlgorithmManager, blobWriteSessionConfig, openTelemetry, baseHashCode()); } @Override @@ -185,10 +125,6 @@ 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(meterProvider, that.meterProvider) - && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -221,10 +157,6 @@ public static class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = HttpStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = HttpStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); - private Boolean enableOtelMetrics = null; - private Boolean enableOtelDebugMetrics = null; - private SdkMeterProvider meterProvider = null; - private Duration metricInterval = Duration.ofSeconds(60); Builder() {} @@ -234,10 +166,6 @@ public static class Builder extends StorageOptions.Builder { this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; this.blobWriteSessionConfig = hso.blobWriteSessionConfig; this.openTelemetry = hso.getOpenTelemetry(); - this.enableOtelMetrics = hso.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = hso.isEnableOtelDebugMetrics(); - this.meterProvider = hso.getMeterProvider(); - this.metricInterval = hso.getMetricInterval(); } @Override @@ -389,58 +317,6 @@ public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) this.openTelemetry = openTelemetry; return this; } - - /** - * Enable or disable OpenTelemetry client metrics. - * - * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public HttpStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { - this.enableOtelDebugMetrics = enableOtelDebugMetrics; - return this; - } - - /** - * Set a custom {@link SdkMeterProvider} for recording client metrics. - * - * @param meterProvider custom SdkMeterProvider to use - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public HttpStorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider) { - this.meterProvider = meterProvider; - return this; - } - - /** - * Set the metric export interval for periodic metric reading. - * - * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public HttpStorageOptions.Builder setMetricInterval(Duration metricInterval) { - this.metricInterval = requireNonNull(metricInterval, "metricInterval must be non null"); - return this; - } } public static final class HttpStorageDefaults extends StorageDefaults { diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 34ee219c8310..89fc78ac5dfc 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -213,6 +213,7 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION, @@ -221,7 +222,9 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) static final ImmutableList CLIENT_SIZE_HISTOGRAMS = ImmutableList.of( StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE); + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED); @VisibleForTesting static SdkMeterProvider createMeterProvider( diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index a62aad85a213..0be29cf215a9 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -48,14 +48,22 @@ final class StorageClientMetrics { static final String METRIC_GCP_STORAGE_CLIENT_GFE_DURATION = "gcp.storage.client.gfe.duration"; static final String METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING = "gcp.storage.client.gfe.header_missing"; + static final String METRIC_GCP_STORAGE_CLIENT_STALL_DURATION = + "gcp.storage.client.stall.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT = + "gcp.storage.client.network.bytes.sent"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED = + "gcp.storage.client.network.bytes.received"; + static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = + "gcp.storage.client.auth.credential_refresh.duration"; + + // Deferred metrics (Phase 2 placeholders - not currently measurable on Java HTTP/gRPC transports) static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION = "gcp.storage.client.network.dns.lookup.duration"; static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION = "gcp.storage.client.network.tcp.connect.duration"; static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION = "gcp.storage.client.network.tls.handshake.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = - "gcp.storage.client.auth.credential_refresh.duration"; private final DoubleHistogram rpcClientCallDuration; private final DoubleHistogram httpClientRequestDuration; @@ -70,6 +78,9 @@ final class StorageClientMetrics { @Nullable private final LongUpDownCounter requestActive; @Nullable private final DoubleHistogram gfeDuration; @Nullable private final LongCounter gfeHeaderMissing; + @Nullable private final DoubleHistogram stallDuration; + @Nullable private final LongHistogram networkBytesSent; + @Nullable private final LongHistogram networkBytesReceived; @Nullable private final DoubleHistogram dnsLookupDuration; @Nullable private final DoubleHistogram tcpConnectDuration; @Nullable private final DoubleHistogram tlsHandshakeDuration; @@ -94,7 +105,7 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt this.httpClientRequestDuration = meter .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) - .setDescription("Duration of one HTTP client request. Retried not included (Otel)") + .setDescription("Duration of one HTTP client request. Retries not included (Otel)") .setUnit("s") .build(); this.gcpClientRequestDuration = @@ -162,6 +173,26 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt "Number of GCS requests where the X-Goog-Gfe-Service-Time header was missing") .setUnit("1") .build(); + this.stallDuration = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_STALL_DURATION) + .setDescription("Duration of client stall") + .setUnit("s") + .build(); + this.networkBytesSent = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT) + .ofLongs() + .setDescription("Number of wire bytes sent on the network") + .setUnit("By") + .build(); + this.networkBytesReceived = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED) + .ofLongs() + .setDescription("Number of wire bytes received from the network") + .setUnit("By") + .build(); this.dnsLookupDuration = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION) @@ -192,6 +223,9 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt this.requestActive = null; this.gfeDuration = null; this.gfeHeaderMissing = null; + this.stallDuration = null; + this.networkBytesSent = null; + this.networkBytesReceived = null; this.dnsLookupDuration = null; this.tcpConnectDuration = null; this.tlsHandshakeDuration = null; @@ -247,6 +281,18 @@ LongHistogram getResponseBodySize() { return gfeHeaderMissing; } + @Nullable DoubleHistogram getStallDuration() { + return stallDuration; + } + + @Nullable LongHistogram getNetworkBytesSent() { + return networkBytesSent; + } + + @Nullable LongHistogram getNetworkBytesReceived() { + return networkBytesReceived; + } + @Nullable DoubleHistogram getDnsLookupDuration() { return dnsLookupDuration; } 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 deleted file mode 100644 index df1a505a9bf4..000000000000 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * 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.function.Function; - -final class StorageMetricsConfig { - static final String SYS_PROP_ENABLE_OTEL_METRICS = "com.google.cloud.storage.enable_otel_metrics"; - static final String SYS_PROP_ENABLE_OTEL_DEBUG_METRICS = - "com.google.cloud.storage.enable_otel_debug_metrics"; - static final String ENV_ENABLE_OTEL_METRICS_JAVA = "GCP_STORAGE_JAVA_ENABLE_OTEL_METRICS"; - static final String ENV_ENABLE_OTEL_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; - static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; - - private static Function sysPropResolver = System::getProperty; - private static Function envResolver = System::getenv; - - private StorageMetricsConfig() {} - - static boolean isEnableOtelMetrics() { - return isEnableOtelMetrics(sysPropResolver, envResolver); - } - - static boolean isEnableOtelDebugMetrics() { - return isEnableOtelDebugMetrics(sysPropResolver, envResolver); - } - - @VisibleForTesting - static boolean isEnableOtelMetrics( - Function sysProps, Function env) { - String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_METRICS); - if (sysProp != null) { - return Boolean.parseBoolean(sysProp); - } - String javaEnv = env.apply(ENV_ENABLE_OTEL_METRICS_JAVA); - if (javaEnv != null) { - return Boolean.parseBoolean(javaEnv); - } - String fallbackEnv = env.apply(ENV_ENABLE_OTEL_METRICS_FALLBACK); - if (fallbackEnv != null) { - return Boolean.parseBoolean(fallbackEnv); - } - return false; - } - - @VisibleForTesting - static boolean isEnableOtelDebugMetrics( - Function sysProps, Function env) { - String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); - if (sysProp != null) { - return Boolean.parseBoolean(sysProp); - } - String javaEnv = env.apply(ENV_ENABLE_OTEL_DEBUG_METRICS); - if (javaEnv != null) { - return Boolean.parseBoolean(javaEnv); - } - return false; - } - - @VisibleForTesting - static void setResolversForTesting( - Function testSysProps, Function testEnv) { - sysPropResolver = testSysProps != null ? testSysProps : System::getProperty; - envResolver = testEnv != null ? testEnv : System::getenv; - } - - @VisibleForTesting - static void resetResolversForTesting() { - sysPropResolver = System::getProperty; - 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 e6647ef80555..97eecedaeef1 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 @@ -33,14 +33,11 @@ import com.google.cloud.storage.TransportCompatibility.Transport; import com.google.cloud.storage.spi.StorageRpcFactory; import io.opentelemetry.api.OpenTelemetry; -import io.opentelemetry.sdk.metrics.SdkMeterProvider; import java.io.IOException; import java.io.InputStream; -import java.time.Duration; import java.util.Locale; import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; public abstract class StorageOptions extends ServiceOptions { @@ -146,43 +143,6 @@ 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.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics); - - /** - * Enable or disable OpenTelemetry debug client metrics. - * - * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract StorageOptions.Builder setEnableOtelDebugMetrics( - boolean enableOtelDebugMetrics); - - /** - * Set a custom {@link SdkMeterProvider} for recording client metrics. - * - * @param meterProvider custom SdkMeterProvider to use - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract StorageOptions.Builder setMeterProvider(SdkMeterProvider meterProvider); - - /** - * Set the metric export interval for periodic metric reading. - * - * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract StorageOptions.Builder setMetricInterval(Duration metricInterval); - @Override public abstract StorageOptions build(); } @@ -225,38 +185,6 @@ public static String version() { @BetaApi public abstract OpenTelemetry getOpenTelemetry(); - /** - * Whether OpenTelemetry client metrics are enabled. - * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract boolean isEnableOtelMetrics(); - - /** - * Whether OpenTelemetry debug client metrics are enabled. - * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract boolean isEnableOtelDebugMetrics(); - - /** - * The {@link SdkMeterProvider} configured for recording client metrics. - * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract @Nullable SdkMeterProvider getMeterProvider(); - - /** - * The metric export interval configured for periodic metric reading. - * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - public abstract Duration getMetricInterval(); - @SuppressWarnings("unchecked") @Override public abstract StorageOptions.Builder toBuilder(); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index be7043b4e425..1cd409733641 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -53,6 +53,9 @@ public void standardMetrics_initializedWithoutDebug() { assertThat(metrics.getRequestActive()).isNull(); assertThat(metrics.getGfeDuration()).isNull(); assertThat(metrics.getGfeHeaderMissing()).isNull(); + assertThat(metrics.getStallDuration()).isNull(); + assertThat(metrics.getNetworkBytesSent()).isNull(); + assertThat(metrics.getNetworkBytesReceived()).isNull(); assertThat(metrics.getDnsLookupDuration()).isNull(); assertThat(metrics.getTcpConnectDuration()).isNull(); assertThat(metrics.getTlsHandshakeDuration()).isNull(); @@ -80,6 +83,9 @@ public void debugMetrics_initializedWhenEnabled() { assertThat(metrics.getRequestActive()).isNotNull(); assertThat(metrics.getGfeDuration()).isNotNull(); assertThat(metrics.getGfeHeaderMissing()).isNotNull(); + assertThat(metrics.getStallDuration()).isNotNull(); + assertThat(metrics.getNetworkBytesSent()).isNotNull(); + assertThat(metrics.getNetworkBytesReceived()).isNotNull(); assertThat(metrics.getDnsLookupDuration()).isNotNull(); assertThat(metrics.getTcpConnectDuration()).isNotNull(); assertThat(metrics.getTlsHandshakeDuration()).isNotNull(); @@ -102,6 +108,9 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getGcpClientRequestDuration().record(0.345); metrics.getOperationTtfb().record(0.045); metrics.getGfeDuration().record(0.012); + metrics.getStallDuration().record(0.010); + metrics.getNetworkBytesSent().record(1024 * 256); + metrics.getNetworkBytesReceived().record(1024 * 512); metrics.getDnsLookupDuration().record(0.005); metrics.getTcpConnectDuration().record(0.015); metrics.getTlsHandshakeDuration().record(0.025); @@ -148,6 +157,10 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, expectedLatencyBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, + expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, @@ -174,6 +187,14 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, expectedSizeBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT, + expectedSizeBoundaries); + assertHistogramBoundaries( + metricsMap, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED, + expectedSizeBoundaries); // Verify counter metrics assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS); 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 deleted file mode 100644 index 7cb568b29a5b..000000000000 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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 io.opentelemetry.sdk.metrics.SdkMeterProvider; -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() { - System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS); - System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); - 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() { - SdkMeterProvider 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 builder_explicitDisablingOverridesGate() { - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); - - HttpStorageOptions httpOptions = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); - assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); - - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); - } - - @Test - public void developmentGate_systemProperty() { - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS, "true"); - - 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 developmentGate_environmentVariableJava() { - Map env = new HashMap<>(); - env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); - env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true"); - StorageMetricsConfig.setResolversForTesting(k -> null, 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 developmentGate_environmentVariableFallback() { - Map env = new HashMap<>(); - env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_FALLBACK, "true"); - StorageMetricsConfig.setResolversForTesting(k -> null, env::get); - - HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); - assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); - - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isTrue(); - } - - @Test - public void developmentGate_systemPropertyTakesPrecedenceOverEnv() { - Map env = new HashMap<>(); - env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); - Map sysProps = new HashMap<>(); - sysProps.put(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); - StorageMetricsConfig.setResolversForTesting(sysProps::get, env::get); - - HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); - assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); - - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); - } -} From babb7022ca458fb276b78aacd20dab3a5fce8cab Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 30 Sep 2026 06:27:55 +0000 Subject: [PATCH 06/28] fix(storage): refactor histogram views and remove deferred connection metrics Refactor addClientHistogramView in OpenTelemetryBootstrappingUtils to remove unit selector and custom view description, preserving instrument default descriptions. Remove deferred Phase 2 connection metrics (dnsLookupDuration, tcpConnectDuration, tlsHandshakeDuration) from StorageClientMetrics, CLIENT_LATENCY_HISTOGRAMS, and StorageClientMetricsTest. Ensure clean Duration imports. [Generated-by: AI] --- .../OpenTelemetryBootstrappingUtils.java | 19 ++------ .../cloud/storage/StorageClientMetrics.java | 44 ------------------- .../storage/StorageClientMetricsTest.java | 21 --------- 3 files changed, 4 insertions(+), 80 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 89fc78ac5dfc..408a73151a0b 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -214,9 +214,6 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION); static final ImmutableList CLIENT_SIZE_HISTOGRAMS = @@ -350,29 +347,21 @@ static SdkMeterProvider createClientMeterProvider( @VisibleForTesting static SdkMeterProviderBuilder registerClientViews(SdkMeterProviderBuilder providerBuilder) { for (String metric : CLIENT_LATENCY_HISTOGRAMS) { - addClientHistogramView(providerBuilder, latencyHistogramBoundaries(), metric, "s"); + addClientHistogramView(providerBuilder, latencyHistogramBoundaries(), metric); } for (String metric : CLIENT_SIZE_HISTOGRAMS) { - addClientHistogramView(providerBuilder, sizeHistogramBoundaries(), metric, "By"); + addClientHistogramView(providerBuilder, sizeHistogramBoundaries(), metric); } return providerBuilder; } private static void addClientHistogramView( - SdkMeterProviderBuilder provider, List boundaries, String name, String unit) { + SdkMeterProviderBuilder provider, List boundaries, String name) { InstrumentSelector instrumentSelector = - InstrumentSelector.builder() - .setType(InstrumentType.HISTOGRAM) - .setUnit(unit) - .setName(name) - .build(); + InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).setName(name).build(); View view = View.builder() .setName(name) - .setDescription( - "A view of " - + name - + " with histogram boundaries more appropriate for Google Cloud Storage RPCs") .setAggregation(Aggregation.explicitBucketHistogram(boundaries)) .build(); provider.registerView(instrumentSelector, view); diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index 0be29cf215a9..db597eb61762 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -57,14 +57,6 @@ final class StorageClientMetrics { static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = "gcp.storage.client.auth.credential_refresh.duration"; - // Deferred metrics (Phase 2 placeholders - not currently measurable on Java HTTP/gRPC transports) - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION = - "gcp.storage.client.network.dns.lookup.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION = - "gcp.storage.client.network.tcp.connect.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION = - "gcp.storage.client.network.tls.handshake.duration"; - private final DoubleHistogram rpcClientCallDuration; private final DoubleHistogram httpClientRequestDuration; private final DoubleHistogram gcpClientRequestDuration; @@ -81,9 +73,6 @@ final class StorageClientMetrics { @Nullable private final DoubleHistogram stallDuration; @Nullable private final LongHistogram networkBytesSent; @Nullable private final LongHistogram networkBytesReceived; - @Nullable private final DoubleHistogram dnsLookupDuration; - @Nullable private final DoubleHistogram tcpConnectDuration; - @Nullable private final DoubleHistogram tlsHandshakeDuration; @Nullable private final DoubleHistogram credentialRefreshDuration; static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOtelDebugMetrics) { @@ -193,24 +182,6 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt .setDescription("Number of wire bytes received from the network") .setUnit("By") .build(); - this.dnsLookupDuration = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION) - .setDescription("Time taken for DNS lookup") - .setUnit("s") - .build(); - this.tcpConnectDuration = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION) - .setDescription("Time taken for TCP connection") - .setUnit("s") - .build(); - this.tlsHandshakeDuration = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION) - .setDescription("Time taken to perform a TLS handshake") - .setUnit("s") - .build(); this.credentialRefreshDuration = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION) @@ -226,9 +197,6 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt this.stallDuration = null; this.networkBytesSent = null; this.networkBytesReceived = null; - this.dnsLookupDuration = null; - this.tcpConnectDuration = null; - this.tlsHandshakeDuration = null; this.credentialRefreshDuration = null; } } @@ -293,18 +261,6 @@ LongHistogram getResponseBodySize() { return networkBytesReceived; } - @Nullable DoubleHistogram getDnsLookupDuration() { - return dnsLookupDuration; - } - - @Nullable DoubleHistogram getTcpConnectDuration() { - return tcpConnectDuration; - } - - @Nullable DoubleHistogram getTlsHandshakeDuration() { - return tlsHandshakeDuration; - } - @Nullable DoubleHistogram getCredentialRefreshDuration() { return credentialRefreshDuration; } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 1cd409733641..ea588f5c1e7e 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -56,9 +56,6 @@ public void standardMetrics_initializedWithoutDebug() { assertThat(metrics.getStallDuration()).isNull(); assertThat(metrics.getNetworkBytesSent()).isNull(); assertThat(metrics.getNetworkBytesReceived()).isNull(); - assertThat(metrics.getDnsLookupDuration()).isNull(); - assertThat(metrics.getTcpConnectDuration()).isNull(); - assertThat(metrics.getTlsHandshakeDuration()).isNull(); assertThat(metrics.getCredentialRefreshDuration()).isNull(); } @@ -86,9 +83,6 @@ public void debugMetrics_initializedWhenEnabled() { assertThat(metrics.getStallDuration()).isNotNull(); assertThat(metrics.getNetworkBytesSent()).isNotNull(); assertThat(metrics.getNetworkBytesReceived()).isNotNull(); - assertThat(metrics.getDnsLookupDuration()).isNotNull(); - assertThat(metrics.getTcpConnectDuration()).isNotNull(); - assertThat(metrics.getTlsHandshakeDuration()).isNotNull(); assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); } @@ -111,9 +105,6 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getStallDuration().record(0.010); metrics.getNetworkBytesSent().record(1024 * 256); metrics.getNetworkBytesReceived().record(1024 * 512); - metrics.getDnsLookupDuration().record(0.005); - metrics.getTcpConnectDuration().record(0.015); - metrics.getTlsHandshakeDuration().record(0.025); metrics.getCredentialRefreshDuration().record(0.080); metrics.getRequestBodySize().record(1024 * 512); @@ -161,18 +152,6 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, expectedLatencyBoundaries); - assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_DNS_LOOKUP_DURATION, - expectedLatencyBoundaries); - assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TCP_CONNECT_DURATION, - expectedLatencyBoundaries); - assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_TLS_HANDSHAKE_DURATION, - expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION, From 22a233fadcf17b1e51e8c933625c482ef0be2871 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 7 Oct 2026 06:58:48 +0000 Subject: [PATCH 07/28] fix(storage): support transport-derived api label in createClientMeterProvider Default api parameter to "grpc" instead of "storage" in createClientMeterProvider, allow specifying "json" / "http" transport protocols for Cloud Monitoring monitored resource storage.googleapis.com/Client, and add unit test coverage in OpenTelemetryBootstrappingUtilsTest. [Generated-by: AI] --- .../OpenTelemetryBootstrappingUtils.java | 29 ++++++++-- .../OpenTelemetryBootstrappingUtilsTest.java | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 408a73151a0b..7a6c465428cc 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -223,6 +223,8 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED); + private static final String DEFAULT_API = "grpc"; + @VisibleForTesting static SdkMeterProvider createMeterProvider( String metricServiceEndpoint, @@ -235,9 +237,14 @@ static SdkMeterProvider createMeterProvider( detectedAttributes, Duration.ofSeconds(60), shouldSuppressExceptions, - "grpc"); + DEFAULT_API); } + /** + * Creates an {@link SdkMeterProvider} configured for the {@code storage.googleapis.com/Client} + * monitored resource with client metric views and Cloud Monitoring exporter, defaulting the + * {@code api} resource label to {@code "grpc"}. + */ @VisibleForTesting static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, @@ -251,9 +258,23 @@ static SdkMeterProvider createClientMeterProvider( detectedAttributes, metricInterval, shouldSuppressExceptions, - "storage"); + DEFAULT_API); } + /** + * Creates an {@link SdkMeterProvider} configured for the {@code storage.googleapis.com/Client} + * monitored resource with client metric views and Cloud Monitoring exporter. + * + * @param metricServiceEndpoint the Cloud Monitoring service endpoint to export metrics to + * @param projectIdToUse the GCP project ID to associate with the monitored resource + * @param detectedAttributes environment-detected resource attributes + * @param metricInterval export interval for the periodic metric reader + * @param shouldSuppressExceptions whether exporter exceptions should be suppressed + * @param api the transport/API protocol distinguishing the client in Cloud Monitoring (e.g., + * {@code "grpc"} for gRPC transport or {@code "json"} for HTTP/JSON transport). Defaults to + * {@code "grpc"} if null. + * @return the configured {@link SdkMeterProvider} + */ @VisibleForTesting static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, @@ -261,7 +282,7 @@ static SdkMeterProvider createClientMeterProvider( Attributes detectedAttributes, Duration metricInterval, boolean shouldSuppressExceptions, - String api) { + @Nullable String api) { MonitoredResourceDescription monitoredResourceDescription = new MonitoredResourceDescription( @@ -315,7 +336,7 @@ static SdkMeterProvider createClientMeterProvider( .put("gcp.resource_type", "storage.googleapis.com/Client") .put("project_id", projectIdToUse) .put("instance_id", UUID.randomUUID().toString()) - .put("api", api != null ? api : "storage"); + .put("api", api != null ? api : DEFAULT_API); String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); if (detectedLocation != null) { attributesBuilder.put("location", detectedLocation); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index 73799e05dac3..3fdc9b7cbd4c 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -22,6 +22,9 @@ import com.google.cloud.storage.OpenTelemetryBootstrappingUtils.ChannelConfigurator; import io.grpc.ManagedChannelBuilder; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import java.time.Duration; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; @@ -119,4 +122,58 @@ public void registerClientViews_doesNotThrow() { OpenTelemetryBootstrappingUtils.registerClientViews(builder); assertThat(returned).isSameInstanceAs(builder); } + + @Test + public void createClientMeterProvider_attachesApiAttribute() { + Attributes detectedAttributes = Attributes.empty(); + + try (SdkMeterProvider defaultProvider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + detectedAttributes, + Duration.ofSeconds(60), + true)) { + assertThat(defaultProvider.toString()).contains("api=\"grpc\""); + } + + try (SdkMeterProvider grpcProvider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + detectedAttributes, + Duration.ofSeconds(60), + true, + "grpc")) { + assertThat(grpcProvider.toString()).contains("api=\"grpc\""); + } + + try (SdkMeterProvider jsonProvider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + detectedAttributes, + Duration.ofSeconds(60), + true, + "json")) { + assertThat(jsonProvider.toString()).contains("api=\"json\""); + } + + try (SdkMeterProvider nullApiProvider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + detectedAttributes, + Duration.ofSeconds(60), + true, + null)) { + assertThat(nullApiProvider.toString()).contains("api=\"grpc\""); + } + + try (SdkMeterProvider legacyProvider = + OpenTelemetryBootstrappingUtils.createMeterProvider( + "monitoring.googleapis.com:443", "test-project", detectedAttributes, true)) { + assertThat(legacyProvider.toString()).contains("api=\"grpc\""); + } + } } From 1f2c14688749989d360f21377641e59bcbe1a14d Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Thu, 8 Oct 2026 06:40:17 +0000 Subject: [PATCH 08/28] fix(storage): address review comments for OpenTelemetry client metrics --- .../OpenTelemetryBootstrappingUtils.java | 145 ++++++++++----- .../cloud/storage/StorageClientMetrics.java | 169 +++++++++++++---- .../OpenTelemetryBootstrappingUtilsTest.java | 37 ++-- .../storage/StorageClientMetricsTest.java | 172 +++++++++++++++--- 4 files changed, 388 insertions(+), 135 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 7a6c465428cc..f8344a772834 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; @@ -212,18 +213,14 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION); static final ImmutableList CLIENT_SIZE_HISTOGRAMS = ImmutableList.of( StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED); - - private static final String DEFAULT_API = "grpc"; + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE); @VisibleForTesting static SdkMeterProvider createMeterProvider( @@ -231,34 +228,97 @@ static SdkMeterProvider createMeterProvider( String projectIdToUse, Attributes detectedAttributes, boolean shouldSuppressExceptions) { - return createClientMeterProvider( - metricServiceEndpoint, - projectIdToUse, - detectedAttributes, - Duration.ofSeconds(60), - shouldSuppressExceptions, - DEFAULT_API); + + MonitoredResourceDescription monitoredResourceDescription = + new MonitoredResourceDescription( + "storage.googleapis.com/Client", + ImmutableSet.of( + "project_id", "location", "cloud_platform", "host_id", "instance_id", "api")); + + MetricExporter cloudMonitoringExporter = + GoogleCloudMetricExporter.createWithConfiguration( + MetricConfiguration.builder() + .setMonitoredResourceDescription(monitoredResourceDescription) + .setInstrumentationLibraryLabelsEnabled(false) + .setMetricServiceEndpoint(metricServiceEndpoint) + .setPrefix("storage.googleapis.com/client") + .setUseServiceTimeSeries(true) + .setProjectId(projectIdToUse) + .build()); + + SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder(); + + // This replaces the dots with slashes in each metric, which is the format needed for this + // monitored resource + for (String metric : + ImmutableList.copyOf(Iterables.concat(METRICS_TO_ENABLE, METRICS_ENABLED_BY_DEFAULT))) { + providerBuilder.registerView( + InstrumentSelector.builder().setName(metric).build(), + View.builder().setName(metric.replace(".", "/")).build()); + } + addHistogramView( + providerBuilder, latencyHistogramBoundaries(), "grpc/client/attempt/duration", "s"); + addHistogramView( + providerBuilder, + sizeHistogramBoundaries(), + "grpc/client/attempt/rcvd_total_compressed_message_size", + "By"); + addHistogramView( + providerBuilder, + sizeHistogramBoundaries(), + "grpc/client/attempt/sent_total_compressed_message_size", + "By"); + + MetricExporter exporter = + shouldSuppressExceptions + ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter) + : cloudMonitoringExporter; + AttributesBuilder attributesBuilder = + Attributes.builder() + .put("gcp.resource_type", "storage.googleapis.com/Client") + .put("project_id", projectIdToUse) + .put("instance_id", UUID.randomUUID().toString()) + .put("api", "grpc"); + String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); + if (detectedLocation != null) { + attributesBuilder.put("location", detectedLocation); + } else { + attributesBuilder.put("location", "global"); + } + String detectedCloudPlatform = detectedAttributes.get(AttributeKey.stringKey("cloud.platform")); + if (detectedCloudPlatform != null) { + attributesBuilder.put("cloud_platform", detectedCloudPlatform); + } else { + attributesBuilder.put("cloud_platform", "unknown"); + } + String detectedHostId = detectedAttributes.get(AttributeKey.stringKey("host.id")); + if (detectedHostId != null) { + attributesBuilder.put("host_id", detectedHostId); + } else { + attributesBuilder.put("host_id", "unknown"); + } + providerBuilder + .registerMetricReader( + PeriodicMetricReader.builder(exporter).setInterval(Duration.ofSeconds(60)).build()) + .setResource(Resource.create(attributesBuilder.build())); + + return providerBuilder.build(); } - /** - * Creates an {@link SdkMeterProvider} configured for the {@code storage.googleapis.com/Client} - * monitored resource with client metric views and Cloud Monitoring exporter, defaulting the - * {@code api} resource label to {@code "grpc"}. - */ @VisibleForTesting static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, String projectIdToUse, Attributes detectedAttributes, - Duration metricInterval, - boolean shouldSuppressExceptions) { + boolean shouldSuppressExceptions, + String api) { return createClientMeterProvider( metricServiceEndpoint, projectIdToUse, detectedAttributes, - metricInterval, + Duration.ofSeconds(60), shouldSuppressExceptions, - DEFAULT_API); + api); } /** @@ -271,8 +331,7 @@ static SdkMeterProvider createClientMeterProvider( * @param metricInterval export interval for the periodic metric reader * @param shouldSuppressExceptions whether exporter exceptions should be suppressed * @param api the transport/API protocol distinguishing the client in Cloud Monitoring (e.g., - * {@code "grpc"} for gRPC transport or {@code "json"} for HTTP/JSON transport). Defaults to - * {@code "grpc"} if null. + * {@code "grpc"} for gRPC transport or {@code "json"} for HTTP/JSON transport). * @return the configured {@link SdkMeterProvider} */ @VisibleForTesting @@ -282,7 +341,8 @@ static SdkMeterProvider createClientMeterProvider( Attributes detectedAttributes, Duration metricInterval, boolean shouldSuppressExceptions, - @Nullable String api) { + String api) { + Objects.requireNonNull(api, "api must not be null"); MonitoredResourceDescription monitoredResourceDescription = new MonitoredResourceDescription( @@ -303,27 +363,6 @@ static SdkMeterProvider createClientMeterProvider( SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder(); - // This replaces the dots with slashes in each metric, which is the format needed for this - // monitored resource - for (String metric : - ImmutableList.copyOf(Iterables.concat(METRICS_TO_ENABLE, METRICS_ENABLED_BY_DEFAULT))) { - providerBuilder.registerView( - InstrumentSelector.builder().setName(metric).build(), - View.builder().setName(metric.replace(".", "/")).build()); - } - addHistogramView( - providerBuilder, latencyHistogramBoundaries(), "grpc/client/attempt/duration", "s"); - addHistogramView( - providerBuilder, - sizeHistogramBoundaries(), - "grpc/client/attempt/rcvd_total_compressed_message_size", - "By"); - addHistogramView( - providerBuilder, - sizeHistogramBoundaries(), - "grpc/client/attempt/sent_total_compressed_message_size", - "By"); - // Register views for client histograms registerClientViews(providerBuilder); @@ -336,7 +375,7 @@ static SdkMeterProvider createClientMeterProvider( .put("gcp.resource_type", "storage.googleapis.com/Client") .put("project_id", projectIdToUse) .put("instance_id", UUID.randomUUID().toString()) - .put("api", api != null ? api : DEFAULT_API); + .put("api", api); String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); if (detectedLocation != null) { attributesBuilder.put("location", detectedLocation); @@ -379,10 +418,14 @@ static SdkMeterProviderBuilder registerClientViews(SdkMeterProviderBuilder provi private static void addClientHistogramView( SdkMeterProviderBuilder provider, List boundaries, String name) { InstrumentSelector instrumentSelector = - InstrumentSelector.builder().setType(InstrumentType.HISTOGRAM).setName(name).build(); + InstrumentSelector.builder() + .setType(InstrumentType.HISTOGRAM) + .setMeterName(StorageClientMetrics.METER_NAME) + .setName(name) + .build(); View view = View.builder() - .setName(name) + .setName(name.replace(".", "/")) .setAggregation(Aggregation.explicitBucketHistogram(boundaries)) .build(); provider.registerView(instrumentSelector, view); @@ -457,6 +500,10 @@ static List sizeHistogramBoundaries() { return boundaries; } + static List sizeHistogramLongBoundaries() { + return sizeHistogramBoundaries().stream().map(Double::longValue).collect(Collectors.toList()); + } + private static final class PermissionDeniedSingleReportMetricsExporter implements MetricExporter { private final MetricExporter delegate; private final AtomicBoolean seenPermissionDenied = new AtomicBoolean(false); diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index db597eb61762..62c1000e9202 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -16,18 +16,40 @@ package com.google.cloud.storage; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; import io.opentelemetry.api.metrics.DoubleHistogram; import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.MeterProvider; +import java.util.UUID; import org.checkerframework.checker.nullness.qual.Nullable; /** Package-private instrument registry for OpenTelemetry client metrics. */ final class StorageClientMetrics { static final String METER_NAME = "com.google.cloud.storage"; + // Standard client attribute keys and values + static final AttributeKey KEY_GCP_CLIENT_VERSION = + AttributeKey.stringKey("gcp.client.version"); + static final AttributeKey KEY_GCP_CLIENT_SERVICE = + AttributeKey.stringKey("gcp.client.service"); + static final AttributeKey KEY_GCP_CLIENT_ARTIFACT = + AttributeKey.stringKey("gcp.client.artifact"); + static final AttributeKey KEY_GCP_CLIENT_INSTANCE_ID = + AttributeKey.stringKey("gcp.client.instance_id"); + + static final String ATTRIBUTE_GCP_CLIENT_VERSION = "gcp.client.version"; + static final String ATTRIBUTE_GCP_CLIENT_SERVICE = "gcp.client.service"; + static final String ATTRIBUTE_GCP_CLIENT_ARTIFACT = "gcp.client.artifact"; + static final String ATTRIBUTE_GCP_CLIENT_INSTANCE_ID = "gcp.client.instance_id"; + + static final String SERVICE_STORAGE = "storage"; + static final String ARTIFACT_STORAGE = "com.google.cloud:google-cloud-storage"; + // Standard metric names static final String METRIC_RPC_CLIENT_CALL_DURATION = "rpc.client.call.duration"; static final String METRIC_HTTP_CLIENT_REQUEST_DURATION = "http.client.request.duration"; @@ -45,9 +67,10 @@ final class StorageClientMetrics { // Debug metric names static final String METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE = "gcp.storage.client.request.active"; - static final String METRIC_GCP_STORAGE_CLIENT_GFE_DURATION = "gcp.storage.client.gfe.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING = - "gcp.storage.client.gfe.header_missing"; + static final String METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION = + "gcp.storage.client.server.duration"; + static final String METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED = + "gcp.storage.client.server.unreached"; static final String METRIC_GCP_STORAGE_CLIENT_STALL_DURATION = "gcp.storage.client.stall.duration"; static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT = @@ -68,40 +91,73 @@ final class StorageClientMetrics { private final LongHistogram responseBodySize; @Nullable private final LongUpDownCounter requestActive; - @Nullable private final DoubleHistogram gfeDuration; - @Nullable private final LongCounter gfeHeaderMissing; + @Nullable private final DoubleHistogram serverDuration; + @Nullable private final LongCounter serverUnreached; @Nullable private final DoubleHistogram stallDuration; - @Nullable private final LongHistogram networkBytesSent; - @Nullable private final LongHistogram networkBytesReceived; + @Nullable private final LongCounter networkBytesSent; + @Nullable private final LongCounter networkBytesReceived; @Nullable private final DoubleHistogram credentialRefreshDuration; + private final Attributes clientAttributes; + static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOtelDebugMetrics) { + return create(meterProvider, UUID.randomUUID().toString(), enableOtelDebugMetrics); + } + + static StorageClientMetrics create( + MeterProvider meterProvider, String instanceId, boolean enableOtelDebugMetrics) { Meter meter = meterProvider .meterBuilder(METER_NAME) .setInstrumentationVersion(StorageOptions.version()) .build(); - return new StorageClientMetrics(meter, enableOtelDebugMetrics); + Attributes clientAttributes = + Attributes.builder() + .put(KEY_GCP_CLIENT_SERVICE, SERVICE_STORAGE) + .put(KEY_GCP_CLIENT_VERSION, StorageOptions.version()) + .put(KEY_GCP_CLIENT_ARTIFACT, ARTIFACT_STORAGE) + .put(KEY_GCP_CLIENT_INSTANCE_ID, instanceId) + .build(); + return new StorageClientMetrics(meter, clientAttributes, enableOtelDebugMetrics); } StorageClientMetrics(Meter meter, boolean enableOtelDebugMetrics) { + this( + meter, + Attributes.builder() + .put(KEY_GCP_CLIENT_SERVICE, SERVICE_STORAGE) + .put(KEY_GCP_CLIENT_VERSION, StorageOptions.version()) + .put(KEY_GCP_CLIENT_ARTIFACT, ARTIFACT_STORAGE) + .put(KEY_GCP_CLIENT_INSTANCE_ID, UUID.randomUUID().toString()) + .build(), + enableOtelDebugMetrics); + } + + StorageClientMetrics(Meter meter, Attributes clientAttributes, boolean enableOtelDebugMetrics) { + this.clientAttributes = clientAttributes; this.rpcClientCallDuration = meter .histogramBuilder(METRIC_RPC_CLIENT_CALL_DURATION) .setDescription("Duration of one gRPC request. Retries not included (Otel)") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); this.httpClientRequestDuration = meter .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) .setDescription("Duration of one HTTP client request. Retries not included (Otel)") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); this.gcpClientRequestDuration = meter .histogramBuilder(METRIC_GCP_CLIENT_REQUEST_DURATION) - .setDescription("Latency of a client operation") + .setDescription("Latency of a client operation, including all retries.") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); this.operations = meter @@ -112,34 +168,48 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt this.attempts = meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ATTEMPTS) - .setDescription("Number of GCS client attempts") + .setDescription( + "Number of GCS client attempts (individual HTTP requests or gRPC calls), including" + + " retries, resumable upload chunks and list pages.") .setUnit("1") .build(); this.errors = meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) - .setDescription("Number of GCS client errors") + .setDescription("Number of failed GCS client attempts, by error.type.") .setUnit("1") .build(); this.operationTtfb = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB) - .setDescription("Time to first byte of GCS client operations") + .setDescription( + "Time from the start of an attempt until the first byte of the response was" + + " received. Not recorded for attempts that received no response.") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); this.requestBodySize = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE) .ofLongs() - .setDescription("Size of GCS client request body") + .setDescription( + "Number of object bytes written by an upload operation (recorded once per" + + " operation, including empty objects).") .setUnit("By") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) .build(); this.responseBodySize = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE) .ofLongs() - .setDescription("Size of GCS client response body") + .setDescription( + "Number of object bytes delivered to the application by a download operation" + + " (recorded once per operation).") .setUnit("By") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) .build(); if (enableOtelDebugMetrics) { @@ -149,51 +219,61 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt .setDescription("Number of active GCS client requests") .setUnit("1") .build(); - this.gfeDuration = + this.serverDuration = meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_GFE_DURATION) - .setDescription("GFE proxy processing time") + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION) + .setDescription( + "Server elapsed processing time for gRPC requests, decoded from" + + " grpc-server-stats-bin response trailer (gRPC only).") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); - this.gfeHeaderMissing = + this.serverUnreached = meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING) + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED) .setDescription( - "Number of GCS requests where the X-Goog-Gfe-Service-Time header was missing") + "Number of attempts that received no response from a GCS server (gRPC:" + + " grpc-server-stats-bin trailer absent; HTTP: X-GUploader-UploadID header" + + " absent).") .setUnit("1") .build(); this.stallDuration = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_STALL_DURATION) - .setDescription("Duration of client stall") + .setDescription( + "Stall timeout after which a read attempt was aborted while waiting for the" + + " initial response.") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); this.networkBytesSent = meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT) - .ofLongs() - .setDescription("Number of wire bytes sent on the network") + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT) + .setDescription("Total bytes sent on the wire (gRPC only).") .setUnit("By") .build(); this.networkBytesReceived = meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED) - .ofLongs() - .setDescription("Number of wire bytes received from the network") + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED) + .setDescription("Total bytes received on the wire (gRPC only).") .setUnit("By") .build(); this.credentialRefreshDuration = meter .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION) .setDescription( - "Duration of the background API/network calls made to refresh OAuth2/JWT access" - + " credentials.") + "Time a request was blocked obtaining an access token. Tokens served from the" + + " credential cache are not recorded.") .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); } else { this.requestActive = null; - this.gfeDuration = null; - this.gfeHeaderMissing = null; + this.serverDuration = null; + this.serverUnreached = null; this.stallDuration = null; this.networkBytesSent = null; this.networkBytesReceived = null; @@ -201,6 +281,21 @@ static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOt } } + Attributes getClientAttributes() { + return clientAttributes; + } + + AttributesBuilder clientAttributesBuilder() { + return clientAttributes.toBuilder(); + } + + Attributes attachClientAttributes(@Nullable Attributes other) { + if (other == null || other.isEmpty()) { + return clientAttributes; + } + return clientAttributes.toBuilder().putAll(other).build(); + } + DoubleHistogram getRpcClientCallDuration() { return rpcClientCallDuration; } @@ -241,23 +336,23 @@ LongHistogram getResponseBodySize() { return requestActive; } - @Nullable DoubleHistogram getGfeDuration() { - return gfeDuration; + @Nullable DoubleHistogram getServerDuration() { + return serverDuration; } - @Nullable LongCounter getGfeHeaderMissing() { - return gfeHeaderMissing; + @Nullable LongCounter getServerUnreached() { + return serverUnreached; } @Nullable DoubleHistogram getStallDuration() { return stallDuration; } - @Nullable LongHistogram getNetworkBytesSent() { + @Nullable LongCounter getNetworkBytesSent() { return networkBytesSent; } - @Nullable LongHistogram getNetworkBytesReceived() { + @Nullable LongCounter getNetworkBytesReceived() { return networkBytesReceived; } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index 3fdc9b7cbd4c..e0340ffc709f 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -17,6 +17,7 @@ package com.google.cloud.storage; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeFalse; import static org.mockito.Mockito.mock; @@ -112,6 +113,12 @@ public void histogramBoundaries_areValidAndIncreasing() { for (int i = 1; i < sizeBoundaries.size(); i++) { assertThat(sizeBoundaries.get(i)).isGreaterThan(sizeBoundaries.get(i - 1)); } + + List sizeLongBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries(); + assertThat(sizeLongBoundaries).hasSize(sizeBoundaries.size()); + for (int i = 1; i < sizeLongBoundaries.size(); i++) { + assertThat(sizeLongBoundaries.get(i)).isGreaterThan(sizeLongBoundaries.get(i - 1)); + } } @Test @@ -127,16 +134,6 @@ public void registerClientViews_doesNotThrow() { public void createClientMeterProvider_attachesApiAttribute() { Attributes detectedAttributes = Attributes.empty(); - try (SdkMeterProvider defaultProvider = - OpenTelemetryBootstrappingUtils.createClientMeterProvider( - "monitoring.googleapis.com:443", - "test-project", - detectedAttributes, - Duration.ofSeconds(60), - true)) { - assertThat(defaultProvider.toString()).contains("api=\"grpc\""); - } - try (SdkMeterProvider grpcProvider = OpenTelemetryBootstrappingUtils.createClientMeterProvider( "monitoring.googleapis.com:443", @@ -159,16 +156,16 @@ public void createClientMeterProvider_attachesApiAttribute() { assertThat(jsonProvider.toString()).contains("api=\"json\""); } - try (SdkMeterProvider nullApiProvider = - OpenTelemetryBootstrappingUtils.createClientMeterProvider( - "monitoring.googleapis.com:443", - "test-project", - detectedAttributes, - Duration.ofSeconds(60), - true, - null)) { - assertThat(nullApiProvider.toString()).contains("api=\"grpc\""); - } + assertThrows( + NullPointerException.class, + () -> + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + detectedAttributes, + Duration.ofSeconds(60), + true, + null)); try (SdkMeterProvider legacyProvider = OpenTelemetryBootstrappingUtils.createMeterProvider( diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index ea588f5c1e7e..0bd8a26e73af 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -18,6 +18,8 @@ import static com.google.common.truth.Truth.assertThat; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.data.HistogramPointData; @@ -49,10 +51,21 @@ public void standardMetrics_initializedWithoutDebug() { assertThat(metrics.getRequestBodySize()).isNotNull(); assertThat(metrics.getResponseBodySize()).isNotNull(); + // Standard client attributes + assertThat(metrics.getClientAttributes()).isNotNull(); + assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_SERVICE)) + .isEqualTo("storage"); + assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_VERSION)) + .isEqualTo(StorageOptions.version()); + assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_ARTIFACT)) + .isEqualTo("com.google.cloud:google-cloud-storage"); + assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_INSTANCE_ID)) + .isNotEmpty(); + // Debug instruments should be null assertThat(metrics.getRequestActive()).isNull(); - assertThat(metrics.getGfeDuration()).isNull(); - assertThat(metrics.getGfeHeaderMissing()).isNull(); + assertThat(metrics.getServerDuration()).isNull(); + assertThat(metrics.getServerUnreached()).isNull(); assertThat(metrics.getStallDuration()).isNull(); assertThat(metrics.getNetworkBytesSent()).isNull(); assertThat(metrics.getNetworkBytesReceived()).isNull(); @@ -78,8 +91,8 @@ public void debugMetrics_initializedWhenEnabled() { // Debug instruments should be present assertThat(metrics.getRequestActive()).isNotNull(); - assertThat(metrics.getGfeDuration()).isNotNull(); - assertThat(metrics.getGfeHeaderMissing()).isNotNull(); + assertThat(metrics.getServerDuration()).isNotNull(); + assertThat(metrics.getServerUnreached()).isNotNull(); assertThat(metrics.getStallDuration()).isNotNull(); assertThat(metrics.getNetworkBytesSent()).isNotNull(); assertThat(metrics.getNetworkBytesReceived()).isNotNull(); @@ -101,10 +114,10 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getHttpClientRequestDuration().record(0.234); metrics.getGcpClientRequestDuration().record(0.345); metrics.getOperationTtfb().record(0.045); - metrics.getGfeDuration().record(0.012); + metrics.getServerDuration().record(0.012); metrics.getStallDuration().record(0.010); - metrics.getNetworkBytesSent().record(1024 * 256); - metrics.getNetworkBytesReceived().record(1024 * 512); + metrics.getNetworkBytesSent().add(1024 * 256); + metrics.getNetworkBytesReceived().add(1024 * 512); metrics.getCredentialRefreshDuration().record(0.080); metrics.getRequestBodySize().record(1024 * 512); @@ -114,7 +127,7 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getAttempts().add(2); metrics.getErrors().add(1); metrics.getRequestActive().add(1); - metrics.getGfeHeaderMissing().add(1); + metrics.getServerUnreached().add(1); Collection collectedMetrics = reader.collectAllMetrics(); Map metricsMap = @@ -125,54 +138,47 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries(); List expectedSizeBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); - // Verify standard latency histograms have custom latency boundaries + // Verify standard latency histograms have custom latency boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, + StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION.replace(".", "/"), expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, + StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION.replace(".", "/"), expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, + StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION.replace(".", "/"), expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB.replace(".", "/"), expectedLatencyBoundaries); - // Verify debug latency histograms have custom latency boundaries + // Verify debug latency histograms have custom latency boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION.replace(".", "/"), expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION.replace(".", "/"), expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION.replace( + ".", "/"), expectedLatencyBoundaries); - // Verify size histograms have custom size boundaries - assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, - expectedSizeBoundaries); - assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, - expectedSizeBoundaries); + // Verify size histograms have custom size boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE.replace(".", "/"), expectedSizeBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE.replace(".", "/"), expectedSizeBoundaries); // Verify counter metrics @@ -182,7 +188,115 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_GFE_HEADER_MISSING); + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED); + } + + @Test + public void latencyHistogram_recordsSecondsScaleValuesIntoExpectedBuckets() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProviderBuilder providerBuilder = + SdkMeterProvider.builder().registerMetricReader(reader); + OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); + SdkMeterProvider provider = providerBuilder.build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, false); + metrics.getGcpClientRequestDuration().record(0.003); + metrics.getGcpClientRequestDuration().record(1.2); + metrics.getGcpClientRequestDuration().record(45.0); + + Collection collectedMetrics = reader.collectAllMetrics(); + Map metricsMap = + collectedMetrics.stream() + .collect(Collectors.toMap(MetricData::getName, Function.identity())); + + String metricViewName = + StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION.replace(".", "/"); + assertThat(metricsMap).containsKey(metricViewName); + + MetricData metricData = metricsMap.get(metricViewName); + HistogramPointData pointData = metricData.getHistogramData().getPoints().iterator().next(); + + List boundaries = pointData.getBoundaries(); + List counts = pointData.getCounts(); + + int index003 = findBucketIndex(boundaries, 0.003); + int index12 = findBucketIndex(boundaries, 1.2); + int index45 = findBucketIndex(boundaries, 45.0); + + // Verify bucket index for 0.003s is 2 (between 0.002s and 0.004s) + assertThat(index003).isEqualTo(2); + assertThat(boundaries.get(index003 - 1)).isEqualTo(0.002); + assertThat(boundaries.get(index003)).isEqualTo(0.004); + assertThat(counts.get(index003)).isEqualTo(1L); + + // Verify 1.2s lands in its expected bucket + assertThat(boundaries.get(index12)).isEqualTo(1.2); + assertThat(boundaries.get(index12 - 1)).isLessThan(1.2); + assertThat(counts.get(index12)).isEqualTo(1L); + + // Verify 45.0s lands in its expected bucket + assertThat(boundaries.get(index45)).isAtLeast(45.0); + assertThat(boundaries.get(index45 - 1)).isLessThan(45.0); + assertThat(counts.get(index45)).isEqualTo(1L); + + // Total count across all buckets must be 3 + long totalRecorded = counts.stream().mapToLong(Long::longValue).sum(); + assertThat(totalRecorded).isEqualTo(3L); + } + + @Test + public void attachClientAttributes_mergesAdditionalAttributes() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", false); + + Attributes extra = + Attributes.of( + AttributeKey.stringKey("custom.key"), + "custom.value", + StorageClientMetrics.KEY_GCP_CLIENT_SERVICE, + "override-service"); + + Attributes merged = metrics.attachClientAttributes(extra); + + assertThat(merged.get(StorageClientMetrics.KEY_GCP_CLIENT_INSTANCE_ID)) + .isEqualTo("test-instance"); + assertThat(merged.get(StorageClientMetrics.KEY_GCP_CLIENT_VERSION)) + .isEqualTo(StorageOptions.version()); + assertThat(merged.get(StorageClientMetrics.KEY_GCP_CLIENT_ARTIFACT)) + .isEqualTo("com.google.cloud:google-cloud-storage"); + assertThat(merged.get(AttributeKey.stringKey("custom.key"))).isEqualTo("custom.value"); + assertThat(merged.get(StorageClientMetrics.KEY_GCP_CLIENT_SERVICE)) + .isEqualTo("override-service"); + + // Null or empty handling + assertThat(metrics.attachClientAttributes(null)) + .isSameInstanceAs(metrics.getClientAttributes()); + assertThat(metrics.attachClientAttributes(Attributes.empty())) + .isSameInstanceAs(metrics.getClientAttributes()); + } + + @Test + public void clientAttributesBuilder_containsBaseClientAttributes() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", false); + + Attributes built = metrics.clientAttributesBuilder().build(); + assertThat(built).isEqualTo(metrics.getClientAttributes()); + } + + private static int findBucketIndex(List boundaries, double value) { + for (int i = 0; i < boundaries.size(); i++) { + if (value <= boundaries.get(i)) { + return i; + } + } + return boundaries.size(); } private static void assertHistogramBoundaries( From 7828fd250aaffb8f1acb68a7fc9e1358125ad3fa Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 14 Oct 2026 15:54:17 +0000 Subject: [PATCH 09/28] feat(storage): address review feedback on client metrics infra --- .../OpenTelemetryBootstrappingUtils.java | 2 +- .../cloud/storage/StorageClientMetrics.java | 314 +++++++++++------- .../storage/StorageClientMetricsTest.java | 95 ++++-- 3 files changed, 265 insertions(+), 146 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index f8344a772834..f6752347546c 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -212,7 +212,7 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_TTFB, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION); diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index 62c1000e9202..d6227320dde1 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -42,6 +42,38 @@ final class StorageClientMetrics { static final AttributeKey KEY_GCP_CLIENT_INSTANCE_ID = AttributeKey.stringKey("gcp.client.instance_id"); + static final AttributeKey KEY_GCP_STORAGE_OPERATION = + AttributeKey.stringKey("gcp.storage.operation"); + static final AttributeKey KEY_GCP_CLIENT_METHOD = + AttributeKey.stringKey("gcp.client.method"); + static final AttributeKey KEY_RPC_METHOD = + AttributeKey.stringKey("rpc.method"); + static final AttributeKey KEY_RPC_SYSTEM_NAME = + AttributeKey.stringKey("rpc.system.name"); + static final AttributeKey KEY_RPC_RESPONSE_STATUS_CODE = + AttributeKey.stringKey("rpc.response.status_code"); + static final AttributeKey KEY_HTTP_REQUEST_METHOD = + AttributeKey.stringKey("http.request.method"); + static final AttributeKey KEY_HTTP_RESPONSE_STATUS_CODE = + AttributeKey.longKey("http.response.status_code"); + static final AttributeKey KEY_SERVER_ADDRESS = + AttributeKey.stringKey("server.address"); + static final AttributeKey KEY_SERVER_PORT = + AttributeKey.longKey("server.port"); + static final AttributeKey KEY_ERROR_TYPE = + AttributeKey.stringKey("error.type"); + static final AttributeKey KEY_NETWORK_PATH = + AttributeKey.stringKey("network.path"); + static final AttributeKey KEY_NETWORK_IO_DIRECTION = + AttributeKey.stringKey("network.io.direction"); + + static final String NETWORK_PATH_DIRECTPATH = "directpath"; + static final String NETWORK_PATH_CLOUDPATH = "cloudpath"; + static final String NETWORK_PATH_UNKNOWN = "unknown"; + + static final String NETWORK_IO_DIRECTION_RECEIVE = "receive"; + static final String NETWORK_IO_DIRECTION_TRANSMIT = "transmit"; + static final String ATTRIBUTE_GCP_CLIENT_VERSION = "gcp.client.version"; static final String ATTRIBUTE_GCP_CLIENT_SERVICE = "gcp.client.service"; static final String ATTRIBUTE_GCP_CLIENT_ARTIFACT = "gcp.client.artifact"; @@ -56,9 +88,9 @@ final class StorageClientMetrics { static final String METRIC_GCP_CLIENT_REQUEST_DURATION = "gcp.client.request.duration"; static final String METRIC_GCP_STORAGE_CLIENT_OPERATIONS = "gcp.storage.client.operations"; static final String METRIC_GCP_STORAGE_CLIENT_ATTEMPTS = "gcp.storage.client.attempts"; + static final String METRIC_GCP_STORAGE_CLIENT_RETRIES = "gcp.storage.client.retries"; static final String METRIC_GCP_STORAGE_CLIENT_ERRORS = "gcp.storage.client.errors"; - static final String METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB = - "gcp.storage.client.operation.ttfb"; + static final String METRIC_GCP_STORAGE_CLIENT_TTFB = "gcp.storage.client.ttfb"; static final String METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE = "gcp.storage.client.request.body.size"; static final String METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE = @@ -73,39 +105,45 @@ final class StorageClientMetrics { "gcp.storage.client.server.unreached"; static final String METRIC_GCP_STORAGE_CLIENT_STALL_DURATION = "gcp.storage.client.stall.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT = - "gcp.storage.client.network.bytes.sent"; - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED = - "gcp.storage.client.network.bytes.received"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_IO = + "gcp.storage.client.network.io"; static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = "gcp.storage.client.auth.credential_refresh.duration"; - private final DoubleHistogram rpcClientCallDuration; - private final DoubleHistogram httpClientRequestDuration; - private final DoubleHistogram gcpClientRequestDuration; - private final LongCounter operations; - private final LongCounter attempts; - private final LongCounter errors; - private final DoubleHistogram operationTtfb; - private final LongHistogram requestBodySize; - private final LongHistogram responseBodySize; + @Nullable private final DoubleHistogram rpcClientCallDuration; + @Nullable private final DoubleHistogram httpClientRequestDuration; + @Nullable private final DoubleHistogram gcpClientRequestDuration; + @Nullable private final LongCounter operations; + @Nullable private final LongCounter attempts; + @Nullable private final LongCounter retries; + @Nullable private final LongCounter errors; + @Nullable private final DoubleHistogram ttfb; + @Nullable private final LongHistogram requestBodySize; + @Nullable private final LongHistogram responseBodySize; @Nullable private final LongUpDownCounter requestActive; @Nullable private final DoubleHistogram serverDuration; @Nullable private final LongCounter serverUnreached; @Nullable private final DoubleHistogram stallDuration; - @Nullable private final LongCounter networkBytesSent; - @Nullable private final LongCounter networkBytesReceived; + @Nullable private final LongCounter networkIo; @Nullable private final DoubleHistogram credentialRefreshDuration; private final Attributes clientAttributes; - static StorageClientMetrics create(MeterProvider meterProvider, boolean enableOtelDebugMetrics) { - return create(meterProvider, UUID.randomUUID().toString(), enableOtelDebugMetrics); + static StorageClientMetrics create( + MeterProvider meterProvider, boolean enableOtelMetrics, boolean enableOtelDebugMetrics) { + return create( + meterProvider, + UUID.randomUUID().toString(), + enableOtelMetrics, + enableOtelDebugMetrics); } static StorageClientMetrics create( - MeterProvider meterProvider, String instanceId, boolean enableOtelDebugMetrics) { + MeterProvider meterProvider, + String instanceId, + boolean enableOtelMetrics, + boolean enableOtelDebugMetrics) { Meter meter = meterProvider .meterBuilder(METER_NAME) @@ -118,10 +156,12 @@ static StorageClientMetrics create( .put(KEY_GCP_CLIENT_ARTIFACT, ARTIFACT_STORAGE) .put(KEY_GCP_CLIENT_INSTANCE_ID, instanceId) .build(); - return new StorageClientMetrics(meter, clientAttributes, enableOtelDebugMetrics); + return new StorageClientMetrics( + meter, clientAttributes, enableOtelMetrics, enableOtelDebugMetrics); } - StorageClientMetrics(Meter meter, boolean enableOtelDebugMetrics) { + StorageClientMetrics( + Meter meter, boolean enableOtelMetrics, boolean enableOtelDebugMetrics) { this( meter, Attributes.builder() @@ -130,87 +170,114 @@ static StorageClientMetrics create( .put(KEY_GCP_CLIENT_ARTIFACT, ARTIFACT_STORAGE) .put(KEY_GCP_CLIENT_INSTANCE_ID, UUID.randomUUID().toString()) .build(), + enableOtelMetrics, enableOtelDebugMetrics); } - StorageClientMetrics(Meter meter, Attributes clientAttributes, boolean enableOtelDebugMetrics) { + StorageClientMetrics( + Meter meter, + Attributes clientAttributes, + boolean enableOtelMetrics, + boolean enableOtelDebugMetrics) { this.clientAttributes = clientAttributes; - this.rpcClientCallDuration = - meter - .histogramBuilder(METRIC_RPC_CLIENT_CALL_DURATION) - .setDescription("Duration of one gRPC request. Retries not included (Otel)") - .setUnit("s") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) - .build(); - this.httpClientRequestDuration = - meter - .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) - .setDescription("Duration of one HTTP client request. Retries not included (Otel)") - .setUnit("s") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) - .build(); - this.gcpClientRequestDuration = - meter - .histogramBuilder(METRIC_GCP_CLIENT_REQUEST_DURATION) - .setDescription("Latency of a client operation, including all retries.") - .setUnit("s") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) - .build(); - this.operations = - meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATIONS) - .setDescription("Number of GCS client operations") - .setUnit("1") - .build(); - this.attempts = - meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ATTEMPTS) - .setDescription( - "Number of GCS client attempts (individual HTTP requests or gRPC calls), including" - + " retries, resumable upload chunks and list pages.") - .setUnit("1") - .build(); - this.errors = - meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) - .setDescription("Number of failed GCS client attempts, by error.type.") - .setUnit("1") - .build(); - this.operationTtfb = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB) - .setDescription( - "Time from the start of an attempt until the first byte of the response was" - + " received. Not recorded for attempts that received no response.") - .setUnit("s") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) - .build(); - this.requestBodySize = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE) - .ofLongs() - .setDescription( - "Number of object bytes written by an upload operation (recorded once per" - + " operation, including empty objects).") - .setUnit("By") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) - .build(); - this.responseBodySize = - meter - .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE) - .ofLongs() - .setDescription( - "Number of object bytes delivered to the application by a download operation" - + " (recorded once per operation).") - .setUnit("By") - .setExplicitBucketBoundariesAdvice( - OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) - .build(); + + if (enableOtelMetrics) { + this.rpcClientCallDuration = + meter + .histogramBuilder(METRIC_RPC_CLIENT_CALL_DURATION) + .setDescription( + "Duration of a single gRPC client call attempt to Cloud Storage.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) + .build(); + this.httpClientRequestDuration = + meter + .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) + .setDescription( + "Duration of a single HTTP request attempt to Cloud Storage.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) + .build(); + this.gcpClientRequestDuration = + meter + .histogramBuilder(METRIC_GCP_CLIENT_REQUEST_DURATION) + .setDescription("Latency of a client operation, including all retries.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) + .build(); + this.operations = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATIONS) + .setDescription("Number of GCS client operations") + .setUnit("1") + .build(); + this.attempts = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ATTEMPTS) + .setDescription( + "Number of GCS client attempts (individual HTTP requests or gRPC calls), including" + + " retries, resumable upload chunks and list pages.") + .setUnit("1") + .build(); + this.retries = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_RETRIES) + .setDescription("Number of GCS client retry attempts.") + .setUnit("1") + .build(); + this.errors = + meter + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) + .setDescription("Number of failed GCS client attempts, by error.type.") + .setUnit("1") + .build(); + this.ttfb = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_TTFB) + .setDescription( + "Time from the start of an attempt until the first byte of the response was" + + " received. Not recorded for attempts that received no response.") + .setUnit("s") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) + .build(); + this.requestBodySize = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE) + .ofLongs() + .setDescription( + "Number of object bytes written by an upload operation (recorded once per" + + " operation, including empty objects).") + .setUnit("By") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) + .build(); + this.responseBodySize = + meter + .histogramBuilder(METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE) + .ofLongs() + .setDescription( + "Number of object bytes delivered to the application by a download operation" + + " (recorded once per operation).") + .setUnit("By") + .setExplicitBucketBoundariesAdvice( + OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries()) + .build(); + } else { + this.rpcClientCallDuration = null; + this.httpClientRequestDuration = null; + this.gcpClientRequestDuration = null; + this.operations = null; + this.attempts = null; + this.retries = null; + this.errors = null; + this.ttfb = null; + this.requestBodySize = null; + this.responseBodySize = null; + } if (enableOtelDebugMetrics) { this.requestActive = @@ -248,16 +315,12 @@ static StorageClientMetrics create( .setExplicitBucketBoundariesAdvice( OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) .build(); - this.networkBytesSent = + this.networkIo = meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT) - .setDescription("Total bytes sent on the wire (gRPC only).") - .setUnit("By") - .build(); - this.networkBytesReceived = - meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED) - .setDescription("Total bytes received on the wire (gRPC only).") + .counterBuilder(METRIC_GCP_STORAGE_CLIENT_NETWORK_IO) + .setDescription( + "Bytes read from and written to the TCP connections to Cloud Storage, by" + + " network.io.direction (receive, transmit). gRPC only.") .setUnit("By") .build(); this.credentialRefreshDuration = @@ -275,8 +338,7 @@ static StorageClientMetrics create( this.serverDuration = null; this.serverUnreached = null; this.stallDuration = null; - this.networkBytesSent = null; - this.networkBytesReceived = null; + this.networkIo = null; this.credentialRefreshDuration = null; } } @@ -296,39 +358,43 @@ Attributes attachClientAttributes(@Nullable Attributes other) { return clientAttributes.toBuilder().putAll(other).build(); } - DoubleHistogram getRpcClientCallDuration() { + @Nullable DoubleHistogram getRpcClientCallDuration() { return rpcClientCallDuration; } - DoubleHistogram getHttpClientRequestDuration() { + @Nullable DoubleHistogram getHttpClientRequestDuration() { return httpClientRequestDuration; } - DoubleHistogram getGcpClientRequestDuration() { + @Nullable DoubleHistogram getGcpClientRequestDuration() { return gcpClientRequestDuration; } - LongCounter getOperations() { + @Nullable LongCounter getOperations() { return operations; } - LongCounter getAttempts() { + @Nullable LongCounter getAttempts() { return attempts; } - LongCounter getErrors() { + @Nullable LongCounter getRetries() { + return retries; + } + + @Nullable LongCounter getErrors() { return errors; } - DoubleHistogram getOperationTtfb() { - return operationTtfb; + @Nullable DoubleHistogram getTtfb() { + return ttfb; } - LongHistogram getRequestBodySize() { + @Nullable LongHistogram getRequestBodySize() { return requestBodySize; } - LongHistogram getResponseBodySize() { + @Nullable LongHistogram getResponseBodySize() { return responseBodySize; } @@ -348,12 +414,8 @@ LongHistogram getResponseBodySize() { return stallDuration; } - @Nullable LongCounter getNetworkBytesSent() { - return networkBytesSent; - } - - @Nullable LongCounter getNetworkBytesReceived() { - return networkBytesReceived; + @Nullable LongCounter getNetworkIo() { + return networkIo; } @Nullable DoubleHistogram getCredentialRefreshDuration() { diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 0bd8a26e73af..68fe0db6811c 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -39,15 +39,16 @@ public void standardMetrics_initializedWithoutDebug() { InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, false); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, false); assertThat(metrics.getRpcClientCallDuration()).isNotNull(); assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); assertThat(metrics.getOperations()).isNotNull(); assertThat(metrics.getAttempts()).isNotNull(); + assertThat(metrics.getRetries()).isNotNull(); assertThat(metrics.getErrors()).isNotNull(); - assertThat(metrics.getOperationTtfb()).isNotNull(); + assertThat(metrics.getTtfb()).isNotNull(); assertThat(metrics.getRequestBodySize()).isNotNull(); assertThat(metrics.getResponseBodySize()).isNotNull(); @@ -67,8 +68,7 @@ public void standardMetrics_initializedWithoutDebug() { assertThat(metrics.getServerDuration()).isNull(); assertThat(metrics.getServerUnreached()).isNull(); assertThat(metrics.getStallDuration()).isNull(); - assertThat(metrics.getNetworkBytesSent()).isNull(); - assertThat(metrics.getNetworkBytesReceived()).isNull(); + assertThat(metrics.getNetworkIo()).isNull(); assertThat(metrics.getCredentialRefreshDuration()).isNull(); } @@ -77,15 +77,16 @@ public void debugMetrics_initializedWhenEnabled() { InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, true); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, true); assertThat(metrics.getRpcClientCallDuration()).isNotNull(); assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); assertThat(metrics.getOperations()).isNotNull(); assertThat(metrics.getAttempts()).isNotNull(); + assertThat(metrics.getRetries()).isNotNull(); assertThat(metrics.getErrors()).isNotNull(); - assertThat(metrics.getOperationTtfb()).isNotNull(); + assertThat(metrics.getTtfb()).isNotNull(); assertThat(metrics.getRequestBodySize()).isNotNull(); assertThat(metrics.getResponseBodySize()).isNotNull(); @@ -94,11 +95,66 @@ public void debugMetrics_initializedWhenEnabled() { assertThat(metrics.getServerDuration()).isNotNull(); assertThat(metrics.getServerUnreached()).isNotNull(); assertThat(metrics.getStallDuration()).isNotNull(); - assertThat(metrics.getNetworkBytesSent()).isNotNull(); - assertThat(metrics.getNetworkBytesReceived()).isNotNull(); + assertThat(metrics.getNetworkIo()).isNotNull(); assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); } + @Test + public void debugOnly_standardMetricsNull() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, true); + + // Standard instruments should be null + assertThat(metrics.getRpcClientCallDuration()).isNull(); + assertThat(metrics.getHttpClientRequestDuration()).isNull(); + assertThat(metrics.getGcpClientRequestDuration()).isNull(); + assertThat(metrics.getOperations()).isNull(); + assertThat(metrics.getAttempts()).isNull(); + assertThat(metrics.getRetries()).isNull(); + assertThat(metrics.getErrors()).isNull(); + assertThat(metrics.getTtfb()).isNull(); + assertThat(metrics.getRequestBodySize()).isNull(); + assertThat(metrics.getResponseBodySize()).isNull(); + + // Debug instruments should be present + assertThat(metrics.getRequestActive()).isNotNull(); + assertThat(metrics.getServerDuration()).isNotNull(); + assertThat(metrics.getServerUnreached()).isNotNull(); + assertThat(metrics.getStallDuration()).isNotNull(); + assertThat(metrics.getNetworkIo()).isNotNull(); + assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); + } + + @Test + public void neitherEnabled_allInstrumentsNull() { + InMemoryMetricReader reader = InMemoryMetricReader.create(); + SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); + + StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, false); + + // Standard instruments should be null + assertThat(metrics.getRpcClientCallDuration()).isNull(); + assertThat(metrics.getHttpClientRequestDuration()).isNull(); + assertThat(metrics.getGcpClientRequestDuration()).isNull(); + assertThat(metrics.getOperations()).isNull(); + assertThat(metrics.getAttempts()).isNull(); + assertThat(metrics.getRetries()).isNull(); + assertThat(metrics.getErrors()).isNull(); + assertThat(metrics.getTtfb()).isNull(); + assertThat(metrics.getRequestBodySize()).isNull(); + assertThat(metrics.getResponseBodySize()).isNull(); + + // Debug instruments should be null + assertThat(metrics.getRequestActive()).isNull(); + assertThat(metrics.getServerDuration()).isNull(); + assertThat(metrics.getServerUnreached()).isNull(); + assertThat(metrics.getStallDuration()).isNull(); + assertThat(metrics.getNetworkIo()).isNull(); + assertThat(metrics.getCredentialRefreshDuration()).isNull(); + } + @Test public void inMemoryMetricReader_verifiesViewsAndBoundaries() { InMemoryMetricReader reader = InMemoryMetricReader.create(); @@ -107,17 +163,16 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); SdkMeterProvider provider = providerBuilder.build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, true); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, true); // Record data for each instrument metrics.getRpcClientCallDuration().record(0.123); metrics.getHttpClientRequestDuration().record(0.234); metrics.getGcpClientRequestDuration().record(0.345); - metrics.getOperationTtfb().record(0.045); + metrics.getTtfb().record(0.045); metrics.getServerDuration().record(0.012); metrics.getStallDuration().record(0.010); - metrics.getNetworkBytesSent().add(1024 * 256); - metrics.getNetworkBytesReceived().add(1024 * 512); + metrics.getNetworkIo().add(1024 * 256); metrics.getCredentialRefreshDuration().record(0.080); metrics.getRequestBodySize().record(1024 * 512); @@ -125,6 +180,7 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getOperations().add(1); metrics.getAttempts().add(2); + metrics.getRetries().add(1); metrics.getErrors().add(1); metrics.getRequestActive().add(1); metrics.getServerUnreached().add(1); @@ -153,7 +209,7 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATION_TTFB.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_TTFB.replace(".", "/"), expectedLatencyBoundaries); // Verify debug latency histograms have custom latency boundaries and slash names @@ -184,15 +240,14 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { // Verify counter metrics assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS); assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS); + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES); assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS); assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED); assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_SENT); - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_BYTES_RECEIVED); + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO); } @Test @@ -203,7 +258,7 @@ public void latencyHistogram_recordsSecondsScaleValuesIntoExpectedBuckets() { OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); SdkMeterProvider provider = providerBuilder.build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, false); + StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, false); metrics.getGcpClientRequestDuration().record(0.003); metrics.getGcpClientRequestDuration().record(1.2); metrics.getGcpClientRequestDuration().record(45.0); @@ -252,7 +307,8 @@ public void latencyHistogram_recordsSecondsScaleValuesIntoExpectedBuckets() { public void attachClientAttributes_mergesAdditionalAttributes() { InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", false); + StorageClientMetrics metrics = + StorageClientMetrics.create(provider, "test-instance", true, false); Attributes extra = Attributes.of( @@ -284,7 +340,8 @@ public void attachClientAttributes_mergesAdditionalAttributes() { public void clientAttributesBuilder_containsBaseClientAttributes() { InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); - StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", false); + StorageClientMetrics metrics = + StorageClientMetrics.create(provider, "test-instance", true, false); Attributes built = metrics.clientAttributesBuilder().build(); assertThat(built).isEqualTo(metrics.getClientAttributes()); From 983f2486f8086c492bbb4ae8fac054bfced811f4 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 14 Oct 2026 16:10:37 +0000 Subject: [PATCH 10/28] style(storage): format code with fmt-maven-plugin --- .../cloud/storage/StorageClientMetrics.java | 35 ++++++------------- .../storage/StorageClientMetricsTest.java | 3 +- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index d6227320dde1..954ad8269dc3 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -46,24 +46,18 @@ final class StorageClientMetrics { AttributeKey.stringKey("gcp.storage.operation"); static final AttributeKey KEY_GCP_CLIENT_METHOD = AttributeKey.stringKey("gcp.client.method"); - static final AttributeKey KEY_RPC_METHOD = - AttributeKey.stringKey("rpc.method"); - static final AttributeKey KEY_RPC_SYSTEM_NAME = - AttributeKey.stringKey("rpc.system.name"); + static final AttributeKey KEY_RPC_METHOD = AttributeKey.stringKey("rpc.method"); + static final AttributeKey KEY_RPC_SYSTEM_NAME = AttributeKey.stringKey("rpc.system.name"); static final AttributeKey KEY_RPC_RESPONSE_STATUS_CODE = AttributeKey.stringKey("rpc.response.status_code"); static final AttributeKey KEY_HTTP_REQUEST_METHOD = AttributeKey.stringKey("http.request.method"); static final AttributeKey KEY_HTTP_RESPONSE_STATUS_CODE = AttributeKey.longKey("http.response.status_code"); - static final AttributeKey KEY_SERVER_ADDRESS = - AttributeKey.stringKey("server.address"); - static final AttributeKey KEY_SERVER_PORT = - AttributeKey.longKey("server.port"); - static final AttributeKey KEY_ERROR_TYPE = - AttributeKey.stringKey("error.type"); - static final AttributeKey KEY_NETWORK_PATH = - AttributeKey.stringKey("network.path"); + static final AttributeKey KEY_SERVER_ADDRESS = AttributeKey.stringKey("server.address"); + static final AttributeKey KEY_SERVER_PORT = AttributeKey.longKey("server.port"); + static final AttributeKey KEY_ERROR_TYPE = AttributeKey.stringKey("error.type"); + static final AttributeKey KEY_NETWORK_PATH = AttributeKey.stringKey("network.path"); static final AttributeKey KEY_NETWORK_IO_DIRECTION = AttributeKey.stringKey("network.io.direction"); @@ -105,8 +99,7 @@ final class StorageClientMetrics { "gcp.storage.client.server.unreached"; static final String METRIC_GCP_STORAGE_CLIENT_STALL_DURATION = "gcp.storage.client.stall.duration"; - static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_IO = - "gcp.storage.client.network.io"; + static final String METRIC_GCP_STORAGE_CLIENT_NETWORK_IO = "gcp.storage.client.network.io"; static final String METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION = "gcp.storage.client.auth.credential_refresh.duration"; @@ -133,10 +126,7 @@ final class StorageClientMetrics { static StorageClientMetrics create( MeterProvider meterProvider, boolean enableOtelMetrics, boolean enableOtelDebugMetrics) { return create( - meterProvider, - UUID.randomUUID().toString(), - enableOtelMetrics, - enableOtelDebugMetrics); + meterProvider, UUID.randomUUID().toString(), enableOtelMetrics, enableOtelDebugMetrics); } static StorageClientMetrics create( @@ -160,8 +150,7 @@ static StorageClientMetrics create( meter, clientAttributes, enableOtelMetrics, enableOtelDebugMetrics); } - StorageClientMetrics( - Meter meter, boolean enableOtelMetrics, boolean enableOtelDebugMetrics) { + StorageClientMetrics(Meter meter, boolean enableOtelMetrics, boolean enableOtelDebugMetrics) { this( meter, Attributes.builder() @@ -185,8 +174,7 @@ static StorageClientMetrics create( this.rpcClientCallDuration = meter .histogramBuilder(METRIC_RPC_CLIENT_CALL_DURATION) - .setDescription( - "Duration of a single gRPC client call attempt to Cloud Storage.") + .setDescription("Duration of a single gRPC client call attempt to Cloud Storage.") .setUnit("s") .setExplicitBucketBoundariesAdvice( OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) @@ -194,8 +182,7 @@ static StorageClientMetrics create( this.httpClientRequestDuration = meter .histogramBuilder(METRIC_HTTP_CLIENT_REQUEST_DURATION) - .setDescription( - "Duration of a single HTTP request attempt to Cloud Storage.") + .setDescription("Duration of a single HTTP request attempt to Cloud Storage.") .setUnit("s") .setExplicitBucketBoundariesAdvice( OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries()) diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 68fe0db6811c..fcb8f089b312 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -246,8 +246,7 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED); - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO); + assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO); } @Test From 52cd16aa41a58e2447aa9b091f677fa12437ebc5 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Fri, 16 Oct 2026 09:29:10 +0000 Subject: [PATCH 11/28] refactor(storage): address PR review comments for OpenTelemetry client metrics --- .../OpenTelemetryBootstrappingUtils.java | 219 +++++++++++------- .../OpenTelemetryBootstrappingUtilsTest.java | 30 ++- .../storage/StorageClientMetricsTest.java | 47 ++-- 3 files changed, 177 insertions(+), 119 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index f6752347546c..71c368f697e2 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -38,10 +38,12 @@ import io.opentelemetry.sdk.common.export.MemoryMode; import io.opentelemetry.sdk.metrics.Aggregation; import io.opentelemetry.sdk.metrics.InstrumentSelector; +import io.opentelemetry.sdk.metrics.InstrumentSelectorBuilder; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.SdkMeterProvider; import io.opentelemetry.sdk.metrics.SdkMeterProviderBuilder; import io.opentelemetry.sdk.metrics.View; +import io.opentelemetry.sdk.metrics.ViewBuilder; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector; @@ -222,6 +224,16 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE); + static final ImmutableList CLIENT_COUNTERS = + ImmutableList.of( + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED, + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO); + @VisibleForTesting static SdkMeterProvider createMeterProvider( String metricServiceEndpoint, @@ -257,52 +269,39 @@ static SdkMeterProvider createMeterProvider( View.builder().setName(metric.replace(".", "/")).build()); } addHistogramView( - providerBuilder, latencyHistogramBoundaries(), "grpc/client/attempt/duration", "s"); + providerBuilder, + latencyHistogramBoundaries(), + "grpc/client/attempt/duration", + "grpc/client/attempt/duration", + "grpc-java", + "s", + "A view of grpc/client/attempt/duration with histogram boundaries more appropriate for Google Cloud Storage RPCs"); addHistogramView( providerBuilder, sizeHistogramBoundaries(), "grpc/client/attempt/rcvd_total_compressed_message_size", - "By"); + "grpc/client/attempt/rcvd_total_compressed_message_size", + "grpc-java", + "By", + "A view of grpc/client/attempt/rcvd_total_compressed_message_size with histogram boundaries more appropriate for Google Cloud Storage RPCs"); addHistogramView( providerBuilder, sizeHistogramBoundaries(), "grpc/client/attempt/sent_total_compressed_message_size", - "By"); - - MetricExporter exporter = - shouldSuppressExceptions - ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter) - : cloudMonitoringExporter; - AttributesBuilder attributesBuilder = - Attributes.builder() - .put("gcp.resource_type", "storage.googleapis.com/Client") - .put("project_id", projectIdToUse) - .put("instance_id", UUID.randomUUID().toString()) - .put("api", "grpc"); - String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); - if (detectedLocation != null) { - attributesBuilder.put("location", detectedLocation); - } else { - attributesBuilder.put("location", "global"); - } - String detectedCloudPlatform = detectedAttributes.get(AttributeKey.stringKey("cloud.platform")); - if (detectedCloudPlatform != null) { - attributesBuilder.put("cloud_platform", detectedCloudPlatform); - } else { - attributesBuilder.put("cloud_platform", "unknown"); - } - String detectedHostId = detectedAttributes.get(AttributeKey.stringKey("host.id")); - if (detectedHostId != null) { - attributesBuilder.put("host_id", detectedHostId); - } else { - attributesBuilder.put("host_id", "unknown"); - } - providerBuilder - .registerMetricReader( - PeriodicMetricReader.builder(exporter).setInterval(Duration.ofSeconds(60)).build()) - .setResource(Resource.create(attributesBuilder.build())); + "grpc/client/attempt/sent_total_compressed_message_size", + "grpc-java", + "By", + "A view of grpc/client/attempt/sent_total_compressed_message_size with histogram boundaries more appropriate for Google Cloud Storage RPCs"); - return providerBuilder.build(); + return buildMeterProvider( + cloudMonitoringExporter, + providerBuilder, + projectIdToUse, + null, + "grpc", + detectedAttributes, + Duration.ofSeconds(60), + shouldSuppressExceptions); } @VisibleForTesting @@ -315,18 +314,39 @@ static SdkMeterProvider createClientMeterProvider( return createClientMeterProvider( metricServiceEndpoint, projectIdToUse, + null, detectedAttributes, Duration.ofSeconds(60), shouldSuppressExceptions, api); } + @VisibleForTesting + static SdkMeterProvider createClientMeterProvider( + String metricServiceEndpoint, + String projectIdToUse, + Attributes detectedAttributes, + Duration metricInterval, + boolean shouldSuppressExceptions, + String api) { + return createClientMeterProvider( + metricServiceEndpoint, + projectIdToUse, + null, + detectedAttributes, + metricInterval, + shouldSuppressExceptions, + api); + } + /** * Creates an {@link SdkMeterProvider} configured for the {@code storage.googleapis.com/Client} * monitored resource with client metric views and Cloud Monitoring exporter. * * @param metricServiceEndpoint the Cloud Monitoring service endpoint to export metrics to * @param projectIdToUse the GCP project ID to associate with the monitored resource + * @param instanceId the client instance ID to associate with the monitored resource, or null to + * generate a random UUID * @param detectedAttributes environment-detected resource attributes * @param metricInterval export interval for the periodic metric reader * @param shouldSuppressExceptions whether exporter exceptions should be suppressed @@ -338,8 +358,9 @@ static SdkMeterProvider createClientMeterProvider( static SdkMeterProvider createClientMeterProvider( String metricServiceEndpoint, String projectIdToUse, + @Nullable String instanceId, Attributes detectedAttributes, - Duration metricInterval, + @Nullable Duration metricInterval, boolean shouldSuppressExceptions, String api) { Objects.requireNonNull(api, "api must not be null"); @@ -363,9 +384,29 @@ static SdkMeterProvider createClientMeterProvider( SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder(); - // Register views for client histograms + // Register views for client histograms and counters registerClientViews(providerBuilder); + return buildMeterProvider( + cloudMonitoringExporter, + providerBuilder, + projectIdToUse, + instanceId, + api, + detectedAttributes, + metricInterval, + shouldSuppressExceptions); + } + + private static SdkMeterProvider buildMeterProvider( + MetricExporter cloudMonitoringExporter, + SdkMeterProviderBuilder providerBuilder, + String projectIdToUse, + @Nullable String instanceId, + String api, + Attributes detectedAttributes, + @Nullable Duration metricInterval, + boolean shouldSuppressExceptions) { MetricExporter exporter = shouldSuppressExceptions ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter) @@ -374,26 +415,16 @@ static SdkMeterProvider createClientMeterProvider( Attributes.builder() .put("gcp.resource_type", "storage.googleapis.com/Client") .put("project_id", projectIdToUse) - .put("instance_id", UUID.randomUUID().toString()) + .put("instance_id", instanceId != null ? instanceId : UUID.randomUUID().toString()) .put("api", api); String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); - if (detectedLocation != null) { - attributesBuilder.put("location", detectedLocation); - } else { - attributesBuilder.put("location", "global"); - } + attributesBuilder.put("location", detectedLocation != null ? detectedLocation : "global"); String detectedCloudPlatform = detectedAttributes.get(AttributeKey.stringKey("cloud.platform")); - if (detectedCloudPlatform != null) { - attributesBuilder.put("cloud_platform", detectedCloudPlatform); - } else { - attributesBuilder.put("cloud_platform", "unknown"); - } + attributesBuilder.put( + "cloud_platform", detectedCloudPlatform != null ? detectedCloudPlatform : "unknown"); String detectedHostId = detectedAttributes.get(AttributeKey.stringKey("host.id")); - if (detectedHostId != null) { - attributesBuilder.put("host_id", detectedHostId); - } else { - attributesBuilder.put("host_id", "unknown"); - } + attributesBuilder.put("host_id", detectedHostId != null ? detectedHostId : "unknown"); + providerBuilder .registerMetricReader( PeriodicMetricReader.builder(exporter) @@ -406,51 +437,61 @@ static SdkMeterProvider createClientMeterProvider( @VisibleForTesting static SdkMeterProviderBuilder registerClientViews(SdkMeterProviderBuilder providerBuilder) { + for (String metric : CLIENT_COUNTERS) { + providerBuilder.registerView( + InstrumentSelector.builder() + .setMeterName(StorageClientMetrics.METER_NAME) + .setName(metric) + .build(), + View.builder().setName(metric.replace(".", "/")).build()); + } for (String metric : CLIENT_LATENCY_HISTOGRAMS) { - addClientHistogramView(providerBuilder, latencyHistogramBoundaries(), metric); + addHistogramView( + providerBuilder, + latencyHistogramBoundaries(), + metric, + metric.replace(".", "/"), + StorageClientMetrics.METER_NAME, + null, + null); } for (String metric : CLIENT_SIZE_HISTOGRAMS) { - addClientHistogramView(providerBuilder, sizeHistogramBoundaries(), metric); + addHistogramView( + providerBuilder, + sizeHistogramBoundaries(), + metric, + metric.replace(".", "/"), + StorageClientMetrics.METER_NAME, + null, + null); } return providerBuilder; } - private static void addClientHistogramView( - SdkMeterProviderBuilder provider, List boundaries, String name) { - InstrumentSelector instrumentSelector = - InstrumentSelector.builder() - .setType(InstrumentType.HISTOGRAM) - .setMeterName(StorageClientMetrics.METER_NAME) - .setName(name) - .build(); - View view = - View.builder() - .setName(name.replace(".", "/")) - .setAggregation(Aggregation.explicitBucketHistogram(boundaries)) - .build(); - provider.registerView(instrumentSelector, view); - } - private static void addHistogramView( - SdkMeterProviderBuilder provider, List boundaries, String name, String unit) { - InstrumentSelector instrumentSelector = + SdkMeterProviderBuilder provider, + List boundaries, + String instrumentName, + String viewName, + String meterName, + @Nullable String unit, + @Nullable String description) { + InstrumentSelectorBuilder selector = InstrumentSelector.builder() .setType(InstrumentType.HISTOGRAM) - .setUnit(unit) - .setName(name) - .setMeterName("grpc-java") - .setMeterSchemaUrl("") - .build(); - View view = + .setMeterName(meterName) + .setName(instrumentName); + if (unit != null) { + selector.setUnit(unit); + } + ViewBuilder view = View.builder() - .setName(name) - .setDescription( - "A view of " - + name - + " with histogram boundaries more appropriate for Google Cloud Storage RPCs") - .setAggregation(Aggregation.explicitBucketHistogram(boundaries)) - .build(); - provider.registerView(instrumentSelector, view); + .setName(viewName) + .setAggregation(Aggregation.explicitBucketHistogram(boundaries)); + if (description != null) { + view.setDescription(description); + } + provider.registerView(selector.build(), view.build()); } @VisibleForTesting diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index e0340ffc709f..0823ed5da992 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -104,21 +104,15 @@ public void channelConfigurator_andThen_nullsafe() { public void histogramBoundaries_areValidAndIncreasing() { List latencyBoundaries = OpenTelemetryBootstrappingUtils.latencyHistogramBoundaries(); assertThat(latencyBoundaries).isNotEmpty(); - for (int i = 1; i < latencyBoundaries.size(); i++) { - assertThat(latencyBoundaries.get(i)).isGreaterThan(latencyBoundaries.get(i - 1)); - } + assertThat(latencyBoundaries).isInStrictOrder(); List sizeBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramBoundaries(); assertThat(sizeBoundaries).isNotEmpty(); - for (int i = 1; i < sizeBoundaries.size(); i++) { - assertThat(sizeBoundaries.get(i)).isGreaterThan(sizeBoundaries.get(i - 1)); - } + assertThat(sizeBoundaries).isInStrictOrder(); List sizeLongBoundaries = OpenTelemetryBootstrappingUtils.sizeHistogramLongBoundaries(); assertThat(sizeLongBoundaries).hasSize(sizeBoundaries.size()); - for (int i = 1; i < sizeLongBoundaries.size(); i++) { - assertThat(sizeLongBoundaries.get(i)).isGreaterThan(sizeLongBoundaries.get(i - 1)); - } + assertThat(sizeLongBoundaries).isInStrictOrder(); } @Test @@ -173,4 +167,22 @@ public void createClientMeterProvider_attachesApiAttribute() { assertThat(legacyProvider.toString()).contains("api=\"grpc\""); } } + + @Test + public void createClientMeterProvider_customInstanceId_attachesInstanceIdAttribute() { + Attributes detectedAttributes = Attributes.empty(); + String customInstanceId = "custom-instance-12345"; + + try (SdkMeterProvider provider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + customInstanceId, + detectedAttributes, + Duration.ofSeconds(60), + true, + "grpc")) { + assertThat(provider.toString()).contains("instance_id=\"" + customInstanceId + "\""); + } + } } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index fcb8f089b312..63209b212adb 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -197,56 +197,60 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { // Verify standard latency histograms have custom latency boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION.replace(".", "/"), + StorageClientMetrics.METRIC_RPC_CLIENT_CALL_DURATION, expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION.replace(".", "/"), + StorageClientMetrics.METRIC_HTTP_CLIENT_REQUEST_DURATION, expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_CLIENT_REQUEST_DURATION, expectedLatencyBoundaries); assertHistogramBoundaries( - metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_TTFB.replace(".", "/"), - expectedLatencyBoundaries); + metricsMap, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_TTFB, expectedLatencyBoundaries); // Verify debug latency histograms have custom latency boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_DURATION, expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_STALL_DURATION, expectedLatencyBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION.replace( - ".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_AUTH_CREDENTIAL_REFRESH_DURATION, expectedLatencyBoundaries); // Verify size histograms have custom size boundaries and slash names assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE, expectedSizeBoundaries); assertHistogramBoundaries( metricsMap, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE.replace(".", "/"), + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, expectedSizeBoundaries); // Verify counter metrics - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS); - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS); - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES); - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS); assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE); + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS.replace(".", "/")); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS.replace(".", "/")); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES.replace(".", "/")); + assertThat(metricsMap) + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS.replace(".", "/")); + assertThat(metricsMap) + .containsKey( + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE.replace(".", "/")); + assertThat(metricsMap) + .containsKey( + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED.replace(".", "/")); assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED); - assertThat(metricsMap).containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO); + .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO.replace(".", "/")); } @Test @@ -357,8 +361,9 @@ private static int findBucketIndex(List boundaries, double value) { private static void assertHistogramBoundaries( Map metricsMap, String metricName, List expectedBoundaries) { - assertThat(metricsMap).containsKey(metricName); - MetricData metricData = metricsMap.get(metricName); + String viewName = metricName.replace(".", "/"); + assertThat(metricsMap).containsKey(viewName); + MetricData metricData = metricsMap.get(viewName); HistogramPointData pointData = metricData.getHistogramData().getPoints().iterator().next(); assertThat(pointData.getBoundaries()).isEqualTo(expectedBoundaries); } From 18b1e7365921074f085eca5b081cb35693c1e392 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 19 Oct 2026 07:47:32 +0000 Subject: [PATCH 12/28] fix(storage): resolve review comments for client metrics infra Update createClientMeterProvider to use prefix custom.googleapis.com with default monitored resource mapping. Add KEY_URL_TEMPLATE attribute key constant in StorageClientMetrics. [Generated-by: AI] --- .../OpenTelemetryBootstrappingUtils.java | 23 +++++++++---------- .../cloud/storage/StorageClientMetrics.java | 1 + .../OpenTelemetryBootstrappingUtilsTest.java | 6 +++++ .../storage/StorageClientMetricsTest.java | 9 ++++++++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 71c368f697e2..4ed6bbba5a65 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -299,6 +299,7 @@ static SdkMeterProvider createMeterProvider( projectIdToUse, null, "grpc", + "storage.googleapis.com/Client", detectedAttributes, Duration.ofSeconds(60), shouldSuppressExceptions); @@ -340,8 +341,9 @@ static SdkMeterProvider createClientMeterProvider( } /** - * Creates an {@link SdkMeterProvider} configured for the {@code storage.googleapis.com/Client} - * monitored resource with client metric views and Cloud Monitoring exporter. + * Creates an {@link SdkMeterProvider} configured for custom client metrics with the {@code + * custom.googleapis.com} prefix and default monitored resource with client metric views and Cloud + * Monitoring exporter. * * @param metricServiceEndpoint the Cloud Monitoring service endpoint to export metrics to * @param projectIdToUse the GCP project ID to associate with the monitored resource @@ -365,20 +367,13 @@ static SdkMeterProvider createClientMeterProvider( String api) { Objects.requireNonNull(api, "api must not be null"); - MonitoredResourceDescription monitoredResourceDescription = - new MonitoredResourceDescription( - "storage.googleapis.com/Client", - ImmutableSet.of( - "project_id", "location", "cloud_platform", "host_id", "instance_id", "api")); - MetricExporter cloudMonitoringExporter = GoogleCloudMetricExporter.createWithConfiguration( MetricConfiguration.builder() - .setMonitoredResourceDescription(monitoredResourceDescription) .setInstrumentationLibraryLabelsEnabled(false) .setMetricServiceEndpoint(metricServiceEndpoint) - .setPrefix("storage.googleapis.com/client") - .setUseServiceTimeSeries(true) + .setPrefix("custom.googleapis.com") + .setUseServiceTimeSeries(false) .setProjectId(projectIdToUse) .build()); @@ -393,6 +388,7 @@ static SdkMeterProvider createClientMeterProvider( projectIdToUse, instanceId, api, + null, detectedAttributes, metricInterval, shouldSuppressExceptions); @@ -404,6 +400,7 @@ private static SdkMeterProvider buildMeterProvider( String projectIdToUse, @Nullable String instanceId, String api, + @Nullable String customResourceType, Attributes detectedAttributes, @Nullable Duration metricInterval, boolean shouldSuppressExceptions) { @@ -413,10 +410,12 @@ private static SdkMeterProvider buildMeterProvider( : cloudMonitoringExporter; AttributesBuilder attributesBuilder = Attributes.builder() - .put("gcp.resource_type", "storage.googleapis.com/Client") .put("project_id", projectIdToUse) .put("instance_id", instanceId != null ? instanceId : UUID.randomUUID().toString()) .put("api", api); + if (customResourceType != null) { + attributesBuilder.put("gcp.resource_type", customResourceType); + } String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); attributesBuilder.put("location", detectedLocation != null ? detectedLocation : "global"); String detectedCloudPlatform = detectedAttributes.get(AttributeKey.stringKey("cloud.platform")); diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index 954ad8269dc3..27e81511779a 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -54,6 +54,7 @@ final class StorageClientMetrics { AttributeKey.stringKey("http.request.method"); static final AttributeKey KEY_HTTP_RESPONSE_STATUS_CODE = AttributeKey.longKey("http.response.status_code"); + static final AttributeKey KEY_URL_TEMPLATE = AttributeKey.stringKey("url.template"); static final AttributeKey KEY_SERVER_ADDRESS = AttributeKey.stringKey("server.address"); static final AttributeKey KEY_SERVER_PORT = AttributeKey.longKey("server.port"); static final AttributeKey KEY_ERROR_TYPE = AttributeKey.stringKey("error.type"); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index 0823ed5da992..4bcdd32a7587 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -137,6 +137,8 @@ public void createClientMeterProvider_attachesApiAttribute() { true, "grpc")) { assertThat(grpcProvider.toString()).contains("api=\"grpc\""); + assertThat(grpcProvider.toString()) + .doesNotContain("gcp.resource_type=\"storage.googleapis.com/Client\""); } try (SdkMeterProvider jsonProvider = @@ -148,6 +150,8 @@ public void createClientMeterProvider_attachesApiAttribute() { true, "json")) { assertThat(jsonProvider.toString()).contains("api=\"json\""); + assertThat(jsonProvider.toString()) + .doesNotContain("gcp.resource_type=\"storage.googleapis.com/Client\""); } assertThrows( @@ -165,6 +169,8 @@ public void createClientMeterProvider_attachesApiAttribute() { OpenTelemetryBootstrappingUtils.createMeterProvider( "monitoring.googleapis.com:443", "test-project", detectedAttributes, true)) { assertThat(legacyProvider.toString()).contains("api=\"grpc\""); + assertThat(legacyProvider.toString()) + .contains("gcp.resource_type=\"storage.googleapis.com/Client\""); } } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 63209b212adb..91cabdd4bba8 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -350,6 +350,15 @@ public void clientAttributesBuilder_containsBaseClientAttributes() { assertThat(built).isEqualTo(metrics.getClientAttributes()); } + @Test + public void attributeKeys_haveExpectedNames() { + assertThat(StorageClientMetrics.KEY_URL_TEMPLATE.getKey()).isEqualTo("url.template"); + assertThat(StorageClientMetrics.KEY_HTTP_REQUEST_METHOD.getKey()) + .isEqualTo("http.request.method"); + assertThat(StorageClientMetrics.KEY_HTTP_RESPONSE_STATUS_CODE.getKey()) + .isEqualTo("http.response.status_code"); + } + private static int findBucketIndex(List boundaries, double value) { for (int i = 0; i < boundaries.size(); i++) { if (value <= boundaries.get(i)) { From 6e7a5c6d733f53090bb7d5595eeec3b3a67b31a7 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 19 Oct 2026 13:51:16 +0000 Subject: [PATCH 13/28] refactor(storage): preserve detectedAttributes in buildMeterProvider for default monitored resource --- .../OpenTelemetryBootstrappingUtils.java | 28 +++++++++++++------ .../OpenTelemetryBootstrappingUtilsTest.java | 24 ++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index 4ed6bbba5a65..d906a6fb32f4 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -408,20 +408,32 @@ private static SdkMeterProvider buildMeterProvider( shouldSuppressExceptions ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter) : cloudMonitoringExporter; - AttributesBuilder attributesBuilder = - Attributes.builder() - .put("project_id", projectIdToUse) - .put("instance_id", instanceId != null ? instanceId : UUID.randomUUID().toString()) - .put("api", api); + AttributesBuilder attributesBuilder = Attributes.builder(); + if (detectedAttributes != null) { + attributesBuilder.putAll(detectedAttributes); + } + attributesBuilder + .put("project_id", projectIdToUse) + .put("instance_id", instanceId != null ? instanceId : UUID.randomUUID().toString()) + .put("api", api); if (customResourceType != null) { attributesBuilder.put("gcp.resource_type", customResourceType); } - String detectedLocation = detectedAttributes.get(AttributeKey.stringKey("cloud.region")); + String detectedLocation = + detectedAttributes != null + ? detectedAttributes.get(AttributeKey.stringKey("cloud.region")) + : null; attributesBuilder.put("location", detectedLocation != null ? detectedLocation : "global"); - String detectedCloudPlatform = detectedAttributes.get(AttributeKey.stringKey("cloud.platform")); + String detectedCloudPlatform = + detectedAttributes != null + ? detectedAttributes.get(AttributeKey.stringKey("cloud.platform")) + : null; attributesBuilder.put( "cloud_platform", detectedCloudPlatform != null ? detectedCloudPlatform : "unknown"); - String detectedHostId = detectedAttributes.get(AttributeKey.stringKey("host.id")); + String detectedHostId = + detectedAttributes != null + ? detectedAttributes.get(AttributeKey.stringKey("host.id")) + : null; attributesBuilder.put("host_id", detectedHostId != null ? detectedHostId : "unknown"); providerBuilder diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java index 4bcdd32a7587..c5844cfaee41 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtilsTest.java @@ -191,4 +191,28 @@ public void createClientMeterProvider_customInstanceId_attachesInstanceIdAttribu assertThat(provider.toString()).contains("instance_id=\"" + customInstanceId + "\""); } } + + @Test + public void createClientMeterProvider_preservesDetectedAttributes() { + Attributes detectedAttributes = + Attributes.builder() + .put("cloud.platform", "gcp_kubernetes_engine") + .put("cloud.availability_zone", "us-central1-a") + .put("host.id", "test-host-123") + .build(); + + try (SdkMeterProvider provider = + OpenTelemetryBootstrappingUtils.createClientMeterProvider( + "monitoring.googleapis.com:443", + "test-project", + "test-instance", + detectedAttributes, + Duration.ofSeconds(60), + true, + "grpc")) { + assertThat(provider.toString()).contains("cloud.platform=\"gcp_kubernetes_engine\""); + assertThat(provider.toString()).contains("cloud.availability_zone=\"us-central1-a\""); + assertThat(provider.toString()).contains("host.id=\"test-host-123\""); + } + } } From 57fa64a478e1f25eaa558aa12ab39be2a9431476 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 06:05:09 +0000 Subject: [PATCH 14/28] fix(metrics): update metric counter units to match Go SDK and design doc --- .../cloud/storage/StorageClientMetrics.java | 12 +++--- .../storage/StorageClientMetricsTest.java | 43 ++++++++++++------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index 27e81511779a..4d28ef7f279e 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -200,7 +200,7 @@ static StorageClientMetrics create( meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_OPERATIONS) .setDescription("Number of GCS client operations") - .setUnit("1") + .setUnit("{operation}") .build(); this.attempts = meter @@ -208,19 +208,19 @@ static StorageClientMetrics create( .setDescription( "Number of GCS client attempts (individual HTTP requests or gRPC calls), including" + " retries, resumable upload chunks and list pages.") - .setUnit("1") + .setUnit("{attempt}") .build(); this.retries = meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_RETRIES) .setDescription("Number of GCS client retry attempts.") - .setUnit("1") + .setUnit("{retry}") .build(); this.errors = meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) .setDescription("Number of failed GCS client attempts, by error.type.") - .setUnit("1") + .setUnit("{error}") .build(); this.ttfb = meter @@ -272,7 +272,7 @@ static StorageClientMetrics create( meter .upDownCounterBuilder(METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE) .setDescription("Number of active GCS client requests") - .setUnit("1") + .setUnit("{request}") .build(); this.serverDuration = meter @@ -291,7 +291,7 @@ static StorageClientMetrics create( "Number of attempts that received no response from a GCS server (gRPC:" + " grpc-server-stats-bin trailer absent; HTTP: X-GUploader-UploadID header" + " absent).") - .setUnit("1") + .setUnit("{attempt}") .build(); this.stallDuration = meter diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 91cabdd4bba8..f1d5ed66a38a 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -234,21 +234,34 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RESPONSE_BODY_SIZE, expectedSizeBoundaries); - // Verify counter metrics - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS.replace(".", "/")); - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS.replace(".", "/")); - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES.replace(".", "/")); - assertThat(metricsMap) - .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS.replace(".", "/")); - assertThat(metricsMap) - .containsKey( - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE.replace(".", "/")); - assertThat(metricsMap) - .containsKey( - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED.replace(".", "/")); + // Verify counter metrics and units + String operationsName = + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS.replace(".", "/"); + assertThat(metricsMap).containsKey(operationsName); + assertThat(metricsMap.get(operationsName).getUnit()).isEqualTo("{operation}"); + + String attemptsName = StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS.replace(".", "/"); + assertThat(metricsMap).containsKey(attemptsName); + assertThat(metricsMap.get(attemptsName).getUnit()).isEqualTo("{attempt}"); + + String retriesName = StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES.replace(".", "/"); + assertThat(metricsMap).containsKey(retriesName); + assertThat(metricsMap.get(retriesName).getUnit()).isEqualTo("{retry}"); + + String errorsName = StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS.replace(".", "/"); + assertThat(metricsMap).containsKey(errorsName); + assertThat(metricsMap.get(errorsName).getUnit()).isEqualTo("{error}"); + + String requestActiveName = + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE.replace(".", "/"); + assertThat(metricsMap).containsKey(requestActiveName); + assertThat(metricsMap.get(requestActiveName).getUnit()).isEqualTo("{request}"); + + String serverUnreachedName = + StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED.replace(".", "/"); + assertThat(metricsMap).containsKey(serverUnreachedName); + assertThat(metricsMap.get(serverUnreachedName).getUnit()).isEqualTo("{attempt}"); + assertThat(metricsMap) .containsKey(StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_NETWORK_IO.replace(".", "/")); } From f39f525693cc2942ad637a9acc532d3ac0a64bb2 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 09:40:01 +0000 Subject: [PATCH 15/28] test(storage): deduplicate instrument nullability assertions in StorageClientMetricsTest [Generated-by: AI] --- .../storage/StorageClientMetricsTest.java | 116 +++++++----------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index f1d5ed66a38a..51f6bd0aa060 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.common.collect.ImmutableList; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.metrics.SdkMeterProvider; @@ -34,6 +35,28 @@ public final class StorageClientMetricsTest { + private static final List> STANDARD_INSTRUMENTS = + ImmutableList.of( + StorageClientMetrics::getRpcClientCallDuration, + StorageClientMetrics::getHttpClientRequestDuration, + StorageClientMetrics::getGcpClientRequestDuration, + StorageClientMetrics::getOperations, + StorageClientMetrics::getAttempts, + StorageClientMetrics::getRetries, + StorageClientMetrics::getErrors, + StorageClientMetrics::getTtfb, + StorageClientMetrics::getRequestBodySize, + StorageClientMetrics::getResponseBodySize); + + private static final List> DEBUG_INSTRUMENTS = + ImmutableList.of( + StorageClientMetrics::getRequestActive, + StorageClientMetrics::getServerDuration, + StorageClientMetrics::getServerUnreached, + StorageClientMetrics::getStallDuration, + StorageClientMetrics::getNetworkIo, + StorageClientMetrics::getCredentialRefreshDuration); + @Test public void standardMetrics_initializedWithoutDebug() { InMemoryMetricReader reader = InMemoryMetricReader.create(); @@ -41,16 +64,7 @@ public void standardMetrics_initializedWithoutDebug() { StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, false); - assertThat(metrics.getRpcClientCallDuration()).isNotNull(); - assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); - assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); - assertThat(metrics.getOperations()).isNotNull(); - assertThat(metrics.getAttempts()).isNotNull(); - assertThat(metrics.getRetries()).isNotNull(); - assertThat(metrics.getErrors()).isNotNull(); - assertThat(metrics.getTtfb()).isNotNull(); - assertThat(metrics.getRequestBodySize()).isNotNull(); - assertThat(metrics.getResponseBodySize()).isNotNull(); + assertInstruments(metrics, STANDARD_INSTRUMENTS, /* isNull= */ false); // Standard client attributes assertThat(metrics.getClientAttributes()).isNotNull(); @@ -64,12 +78,7 @@ public void standardMetrics_initializedWithoutDebug() { .isNotEmpty(); // Debug instruments should be null - assertThat(metrics.getRequestActive()).isNull(); - assertThat(metrics.getServerDuration()).isNull(); - assertThat(metrics.getServerUnreached()).isNull(); - assertThat(metrics.getStallDuration()).isNull(); - assertThat(metrics.getNetworkIo()).isNull(); - assertThat(metrics.getCredentialRefreshDuration()).isNull(); + assertInstruments(metrics, DEBUG_INSTRUMENTS, /* isNull= */ true); } @Test @@ -79,24 +88,8 @@ public void debugMetrics_initializedWhenEnabled() { StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, true); - assertThat(metrics.getRpcClientCallDuration()).isNotNull(); - assertThat(metrics.getHttpClientRequestDuration()).isNotNull(); - assertThat(metrics.getGcpClientRequestDuration()).isNotNull(); - assertThat(metrics.getOperations()).isNotNull(); - assertThat(metrics.getAttempts()).isNotNull(); - assertThat(metrics.getRetries()).isNotNull(); - assertThat(metrics.getErrors()).isNotNull(); - assertThat(metrics.getTtfb()).isNotNull(); - assertThat(metrics.getRequestBodySize()).isNotNull(); - assertThat(metrics.getResponseBodySize()).isNotNull(); - - // Debug instruments should be present - assertThat(metrics.getRequestActive()).isNotNull(); - assertThat(metrics.getServerDuration()).isNotNull(); - assertThat(metrics.getServerUnreached()).isNotNull(); - assertThat(metrics.getStallDuration()).isNotNull(); - assertThat(metrics.getNetworkIo()).isNotNull(); - assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); + assertInstruments(metrics, STANDARD_INSTRUMENTS, /* isNull= */ false); + assertInstruments(metrics, DEBUG_INSTRUMENTS, /* isNull= */ false); } @Test @@ -106,25 +99,8 @@ public void debugOnly_standardMetricsNull() { StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, true); - // Standard instruments should be null - assertThat(metrics.getRpcClientCallDuration()).isNull(); - assertThat(metrics.getHttpClientRequestDuration()).isNull(); - assertThat(metrics.getGcpClientRequestDuration()).isNull(); - assertThat(metrics.getOperations()).isNull(); - assertThat(metrics.getAttempts()).isNull(); - assertThat(metrics.getRetries()).isNull(); - assertThat(metrics.getErrors()).isNull(); - assertThat(metrics.getTtfb()).isNull(); - assertThat(metrics.getRequestBodySize()).isNull(); - assertThat(metrics.getResponseBodySize()).isNull(); - - // Debug instruments should be present - assertThat(metrics.getRequestActive()).isNotNull(); - assertThat(metrics.getServerDuration()).isNotNull(); - assertThat(metrics.getServerUnreached()).isNotNull(); - assertThat(metrics.getStallDuration()).isNotNull(); - assertThat(metrics.getNetworkIo()).isNotNull(); - assertThat(metrics.getCredentialRefreshDuration()).isNotNull(); + assertInstruments(metrics, STANDARD_INSTRUMENTS, /* isNull= */ true); + assertInstruments(metrics, DEBUG_INSTRUMENTS, /* isNull= */ false); } @Test @@ -134,25 +110,8 @@ public void neitherEnabled_allInstrumentsNull() { StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, false); - // Standard instruments should be null - assertThat(metrics.getRpcClientCallDuration()).isNull(); - assertThat(metrics.getHttpClientRequestDuration()).isNull(); - assertThat(metrics.getGcpClientRequestDuration()).isNull(); - assertThat(metrics.getOperations()).isNull(); - assertThat(metrics.getAttempts()).isNull(); - assertThat(metrics.getRetries()).isNull(); - assertThat(metrics.getErrors()).isNull(); - assertThat(metrics.getTtfb()).isNull(); - assertThat(metrics.getRequestBodySize()).isNull(); - assertThat(metrics.getResponseBodySize()).isNull(); - - // Debug instruments should be null - assertThat(metrics.getRequestActive()).isNull(); - assertThat(metrics.getServerDuration()).isNull(); - assertThat(metrics.getServerUnreached()).isNull(); - assertThat(metrics.getStallDuration()).isNull(); - assertThat(metrics.getNetworkIo()).isNull(); - assertThat(metrics.getCredentialRefreshDuration()).isNull(); + assertInstruments(metrics, STANDARD_INSTRUMENTS, /* isNull= */ true); + assertInstruments(metrics, DEBUG_INSTRUMENTS, /* isNull= */ true); } @Test @@ -389,4 +348,17 @@ private static void assertHistogramBoundaries( HistogramPointData pointData = metricData.getHistogramData().getPoints().iterator().next(); assertThat(pointData.getBoundaries()).isEqualTo(expectedBoundaries); } + + private static void assertInstruments( + StorageClientMetrics metrics, + List> instruments, + boolean isNull) { + for (Function getter : instruments) { + if (isNull) { + assertThat(getter.apply(metrics)).isNull(); + } else { + assertThat(getter.apply(metrics)).isNotNull(); + } + } + } } From a1f434ddaca7cee1badf13b0965e3f3544bad6c8 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 10:08:55 +0000 Subject: [PATCH 16/28] test(storage): initialize InMemoryMetricReader in @Before and clean up comments --- .../storage/StorageClientMetricsTest.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 51f6bd0aa060..49966661f117 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -31,6 +31,7 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; +import org.junit.Before; import org.junit.Test; public final class StorageClientMetricsTest { @@ -57,16 +58,21 @@ public final class StorageClientMetricsTest { StorageClientMetrics::getNetworkIo, StorageClientMetrics::getCredentialRefreshDuration); + private InMemoryMetricReader reader; + + @Before + public void setUp() { + reader = InMemoryMetricReader.create(); + } + @Test public void standardMetrics_initializedWithoutDebug() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, false); assertInstruments(metrics, STANDARD_INSTRUMENTS, /* isNull= */ false); - // Standard client attributes assertThat(metrics.getClientAttributes()).isNotNull(); assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_SERVICE)) .isEqualTo("storage"); @@ -77,13 +83,11 @@ public void standardMetrics_initializedWithoutDebug() { assertThat(metrics.getClientAttributes().get(StorageClientMetrics.KEY_GCP_CLIENT_INSTANCE_ID)) .isNotEmpty(); - // Debug instruments should be null assertInstruments(metrics, DEBUG_INSTRUMENTS, /* isNull= */ true); } @Test public void debugMetrics_initializedWhenEnabled() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, true, true); @@ -94,7 +98,6 @@ public void debugMetrics_initializedWhenEnabled() { @Test public void debugOnly_standardMetricsNull() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, true); @@ -105,7 +108,6 @@ public void debugOnly_standardMetricsNull() { @Test public void neitherEnabled_allInstrumentsNull() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, false, false); @@ -116,7 +118,6 @@ public void neitherEnabled_allInstrumentsNull() { @Test public void inMemoryMetricReader_verifiesViewsAndBoundaries() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder().registerMetricReader(reader); OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); @@ -227,7 +228,6 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { @Test public void latencyHistogram_recordsSecondsScaleValuesIntoExpectedBuckets() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProviderBuilder providerBuilder = SdkMeterProvider.builder().registerMetricReader(reader); OpenTelemetryBootstrappingUtils.registerClientViews(providerBuilder); @@ -280,7 +280,6 @@ public void latencyHistogram_recordsSecondsScaleValuesIntoExpectedBuckets() { @Test public void attachClientAttributes_mergesAdditionalAttributes() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", true, false); @@ -313,7 +312,6 @@ public void attachClientAttributes_mergesAdditionalAttributes() { @Test public void clientAttributesBuilder_containsBaseClientAttributes() { - InMemoryMetricReader reader = InMemoryMetricReader.create(); SdkMeterProvider provider = SdkMeterProvider.builder().registerMetricReader(reader).build(); StorageClientMetrics metrics = StorageClientMetrics.create(provider, "test-instance", true, false); From 4607b23be7da860581d753598cf5588ab73f87ad Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 10:28:00 +0000 Subject: [PATCH 17/28] refactor(storage): remove misleading @VisibleForTesting from histogram boundary helpers --- .../google/cloud/storage/OpenTelemetryBootstrappingUtils.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index d906a6fb32f4..f8e6179af5b6 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -505,7 +505,6 @@ private static void addHistogramView( provider.registerView(selector.build(), view.build()); } - @VisibleForTesting static List latencyHistogramBoundaries() { List boundaries = new ArrayList<>(); BigDecimal boundary = new BigDecimal(0, MathContext.UNLIMITED); @@ -531,7 +530,6 @@ static List latencyHistogramBoundaries() { return boundaries; } - @VisibleForTesting static List sizeHistogramBoundaries() { long kb = 1024; long mb = 1024 * kb; From 97ed050b3316e71daad5af04ba8a31503d1b6e52 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 21 Oct 2026 18:12:02 +0000 Subject: [PATCH 18/28] refactor(storage): remove gcp.storage.client.retries counter [Generated-by: AI] --- .../storage/OpenTelemetryBootstrappingUtils.java | 1 - .../google/cloud/storage/StorageClientMetrics.java | 13 ------------- .../cloud/storage/StorageClientMetricsTest.java | 6 ------ 3 files changed, 20 deletions(-) diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java index f8e6179af5b6..d83ce6bb91d9 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java @@ -228,7 +228,6 @@ static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) ImmutableList.of( StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_OPERATIONS, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ATTEMPTS, - StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_REQUEST_ACTIVE, StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_SERVER_UNREACHED, diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java index 4d28ef7f279e..659fb698dcd4 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageClientMetrics.java @@ -83,7 +83,6 @@ final class StorageClientMetrics { static final String METRIC_GCP_CLIENT_REQUEST_DURATION = "gcp.client.request.duration"; static final String METRIC_GCP_STORAGE_CLIENT_OPERATIONS = "gcp.storage.client.operations"; static final String METRIC_GCP_STORAGE_CLIENT_ATTEMPTS = "gcp.storage.client.attempts"; - static final String METRIC_GCP_STORAGE_CLIENT_RETRIES = "gcp.storage.client.retries"; static final String METRIC_GCP_STORAGE_CLIENT_ERRORS = "gcp.storage.client.errors"; static final String METRIC_GCP_STORAGE_CLIENT_TTFB = "gcp.storage.client.ttfb"; static final String METRIC_GCP_STORAGE_CLIENT_REQUEST_BODY_SIZE = @@ -109,7 +108,6 @@ final class StorageClientMetrics { @Nullable private final DoubleHistogram gcpClientRequestDuration; @Nullable private final LongCounter operations; @Nullable private final LongCounter attempts; - @Nullable private final LongCounter retries; @Nullable private final LongCounter errors; @Nullable private final DoubleHistogram ttfb; @Nullable private final LongHistogram requestBodySize; @@ -210,12 +208,6 @@ static StorageClientMetrics create( + " retries, resumable upload chunks and list pages.") .setUnit("{attempt}") .build(); - this.retries = - meter - .counterBuilder(METRIC_GCP_STORAGE_CLIENT_RETRIES) - .setDescription("Number of GCS client retry attempts.") - .setUnit("{retry}") - .build(); this.errors = meter .counterBuilder(METRIC_GCP_STORAGE_CLIENT_ERRORS) @@ -260,7 +252,6 @@ static StorageClientMetrics create( this.gcpClientRequestDuration = null; this.operations = null; this.attempts = null; - this.retries = null; this.errors = null; this.ttfb = null; this.requestBodySize = null; @@ -366,10 +357,6 @@ Attributes attachClientAttributes(@Nullable Attributes other) { return attempts; } - @Nullable LongCounter getRetries() { - return retries; - } - @Nullable LongCounter getErrors() { return errors; } diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java index 49966661f117..9d3b8fa95e4d 100644 --- a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageClientMetricsTest.java @@ -43,7 +43,6 @@ public final class StorageClientMetricsTest { StorageClientMetrics::getGcpClientRequestDuration, StorageClientMetrics::getOperations, StorageClientMetrics::getAttempts, - StorageClientMetrics::getRetries, StorageClientMetrics::getErrors, StorageClientMetrics::getTtfb, StorageClientMetrics::getRequestBodySize, @@ -140,7 +139,6 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { metrics.getOperations().add(1); metrics.getAttempts().add(2); - metrics.getRetries().add(1); metrics.getErrors().add(1); metrics.getRequestActive().add(1); metrics.getServerUnreached().add(1); @@ -204,10 +202,6 @@ public void inMemoryMetricReader_verifiesViewsAndBoundaries() { assertThat(metricsMap).containsKey(attemptsName); assertThat(metricsMap.get(attemptsName).getUnit()).isEqualTo("{attempt}"); - String retriesName = StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_RETRIES.replace(".", "/"); - assertThat(metricsMap).containsKey(retriesName); - assertThat(metricsMap.get(retriesName).getUnit()).isEqualTo("{retry}"); - String errorsName = StorageClientMetrics.METRIC_GCP_STORAGE_CLIENT_ERRORS.replace(".", "/"); assertThat(metricsMap).containsKey(errorsName); assertThat(metricsMap.get(errorsName).getUnit()).isEqualTo("{error}"); From dc20d62265d37798f0083768932af9c14aeec8d1 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 23 Sep 2026 10:31:15 +0000 Subject: [PATCH 19/28] feat(storage): add OpenTelemetry client metrics configuration and options [Generated-by: AI] --- .../cloud/storage/GrpcStorageOptions.java | 149 ++++++++++- .../cloud/storage/HttpStorageOptions.java | 133 +++++++++- .../cloud/storage/StorageMetricsConfig.java | 87 +++++++ .../google/cloud/storage/StorageOptions.java | 72 ++++++ .../cloud/storage/StorageOptionsTest.java | 242 ++++++++++++++++++ 5 files changed, 670 insertions(+), 13 deletions(-) create mode 100644 java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java create mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java 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..02ac9ef28304 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 @@ -140,7 +142,7 @@ public final class GrpcStorageOptions extends StorageOptions System.getProperty("com.google.cloud.storage.grpc.bound_token", "false")); private final GrpcRetryAlgorithmManager retryAlgorithmManager; - private final java.time.Duration terminationAwaitDuration; + private final Duration terminationAwaitDuration; private final boolean attemptDirectPath; private final boolean enableGrpcClientMetrics; @@ -148,6 +150,10 @@ public final class GrpcStorageOptions extends StorageOptions private final GrpcInterceptorProvider grpcInterceptorProvider; private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; + private final boolean enableOtelMetrics; + private final boolean enableOtelDebugMetrics; + private transient MeterProvider meterProvider; + private final Duration metricInterval; private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -165,6 +171,16 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.grpcInterceptorProvider = builder.grpcInterceptorProvider; this.blobWriteSessionConfig = builder.blobWriteSessionConfig; this.openTelemetry = builder.openTelemetry; + this.enableOtelMetrics = + builder.enableOtelMetrics != null + ? builder.enableOtelMetrics + : StorageMetricsConfig.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = + builder.enableOtelDebugMetrics != null + ? builder.enableOtelDebugMetrics + : StorageMetricsConfig.isEnableOtelDebugMetrics(); + this.meterProvider = builder.meterProvider; + this.metricInterval = builder.metricInterval; } @Override @@ -178,7 +194,7 @@ GrpcRetryAlgorithmManager getRetryAlgorithmManager() { } @InternalApi - java.time.Duration getTerminationAwaitDuration() { + Duration getTerminationAwaitDuration() { return terminationAwaitDuration; } @@ -195,6 +211,7 @@ GrpcInterceptorProvider getGrpcInterceptorProvider() { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); + this.meterProvider = this.openTelemetry.getMeterProvider(); } /** @@ -363,9 +380,9 @@ private Tuple> resolveSettingsAndOpts() throw // seconds. // To allow read streams to have longer lifespans, crank up their timeouts, instead rely // on idleTimeout below. - .setLogicalTimeout(java.time.Duration.ofDays(28)) + .setLogicalTimeout(Duration.ofDays(28)) .build(); - java.time.Duration totalTimeout = baseRetrySettings.getTotalTimeoutDuration(); + Duration totalTimeout = baseRetrySettings.getTotalTimeoutDuration(); // retries for unary methods are generally handled at a different level, except // StartResumableWrite @@ -414,6 +431,45 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelMetrics() { + return enableOtelMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelDebugMetrics() { + return enableOtelDebugMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public @Nullable MeterProvider getMeterProvider() { + if (meterProvider == null && openTelemetry != null) { + return openTelemetry.getMeterProvider(); + } + return meterProvider; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public Duration getMetricInterval() { + return metricInterval; + } + /** * @since 2.14.0 */ @@ -432,6 +488,10 @@ public int hashCode() { grpcInterceptorProvider, blobWriteSessionConfig, openTelemetry, + enableOtelMetrics, + enableOtelDebugMetrics, + meterProvider, + metricInterval, baseHashCode()); } @@ -446,11 +506,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(meterProvider, that.meterProvider) + && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -492,7 +556,7 @@ protected boolean shouldRefreshService(Storage cachedService) { public static final class Builder extends StorageOptions.Builder { private StorageRetryStrategy storageRetryStrategy; - private java.time.Duration terminationAwaitDuration; + private Duration terminationAwaitDuration; private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath(); private boolean enableGrpcClientMetrics = GrpcStorageDefaults.INSTANCE.isEnableGrpcClientMetrics(); @@ -501,6 +565,10 @@ public static final class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = GrpcStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); + private Boolean enableOtelMetrics = null; + private Boolean enableOtelDebugMetrics = null; + private MeterProvider meterProvider = null; + private Duration metricInterval = Duration.ofSeconds(60); private boolean grpcMetricsManuallyEnabled = false; @@ -516,13 +584,16 @@ public static final class Builder extends StorageOptions.Builder { this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; this.openTelemetry = gso.openTelemetry; + this.enableOtelMetrics = gso.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = gso.isEnableOtelDebugMetrics(); + this.meterProvider = gso.getMeterProvider(); + this.metricInterval = gso.getMetricInterval(); } /** - * This method is obsolete. Use {@link #setTerminationAwaitJavaTimeDuration(java.time.Duration)} - * instead. + * This method is obsolete. Use {@link #setTerminationAwaitJavaTimeDuration(Duration)} instead. */ - @ObsoleteApi("Use setTerminationAwaitJavaTimeDuration(java.time.Duration) instead") + @ObsoleteApi("Use setTerminationAwaitJavaTimeDuration(Duration) instead") public Builder setTerminationAwaitDuration(org.threeten.bp.Duration terminationAwaitDuration) { return setTerminationAwaitJavaTimeDuration(toJavaTimeDuration(terminationAwaitDuration)); } @@ -535,8 +606,7 @@ public Builder setTerminationAwaitDuration(org.threeten.bp.Duration terminationA * @return the builder * @since 2.14.0 */ - public Builder setTerminationAwaitJavaTimeDuration( - java.time.Duration terminationAwaitDuration) { + public Builder setTerminationAwaitJavaTimeDuration(Duration terminationAwaitDuration) { this.terminationAwaitDuration = requireNonNull(terminationAwaitDuration, "terminationAwaitDuration must be non null"); return this; @@ -750,6 +820,61 @@ public GrpcStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) return this; } + /** + * Enable or disable OpenTelemetry client metrics. + * + * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { + this.enableOtelDebugMetrics = enableOtelDebugMetrics; + return this; + } + + /** + * Set a custom {@link MeterProvider} for recording client metrics. + * + * @param meterProvider custom MeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setMeterProvider(MeterProvider meterProvider) { + this.meterProvider = meterProvider; + return this; + } + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public GrpcStorageOptions.Builder setMetricInterval(Duration metricInterval) { + checkArgument( + metricInterval != null && !metricInterval.isNegative() && !metricInterval.isZero(), + "metricInterval must be positive"); + this.metricInterval = metricInterval; + return this; + } + /** * @since 2.14.0 */ @@ -817,8 +942,8 @@ public org.threeten.bp.Duration getTerminationAwaitDuration() { /** * @since 2.14.0 */ - public java.time.Duration getTerminationAwaitDurationJavaTime() { - return java.time.Duration.ofMinutes(1); + public Duration getTerminationAwaitDurationJavaTime() { + return Duration.ofMinutes(1); } /** 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..da131282881d 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 @@ -66,6 +69,10 @@ public class HttpStorageOptions extends StorageOptions { private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; + private final boolean enableOtelMetrics; + private final boolean enableOtelDebugMetrics; + private transient MeterProvider meterProvider; + private final Duration metricInterval; private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -76,6 +83,16 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { retryDepsAdapter = new RetryDependenciesAdapter(); blobWriteSessionConfig = builder.blobWriteSessionConfig; openTelemetry = builder.openTelemetry; + this.enableOtelMetrics = + builder.enableOtelMetrics != null + ? builder.enableOtelMetrics + : StorageMetricsConfig.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = + builder.enableOtelDebugMetrics != null + ? builder.enableOtelDebugMetrics + : StorageMetricsConfig.isEnableOtelDebugMetrics(); + this.meterProvider = builder.meterProvider; + this.metricInterval = builder.metricInterval; } @Override @@ -102,6 +119,45 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelMetrics() { + return enableOtelMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public boolean isEnableOtelDebugMetrics() { + return enableOtelDebugMetrics; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public @Nullable MeterProvider getMeterProvider() { + if (meterProvider == null && openTelemetry != null) { + return openTelemetry.getMeterProvider(); + } + return meterProvider; + } + + /** + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public Duration getMetricInterval() { + return metricInterval; + } + @Override public HttpStorageOptions.Builder toBuilder() { return new HttpStorageOptions.Builder(this); @@ -110,7 +166,14 @@ public HttpStorageOptions.Builder toBuilder() { @Override public int hashCode() { return Objects.hash( - retryAlgorithmManager, blobWriteSessionConfig, openTelemetry, baseHashCode()); + retryAlgorithmManager, + blobWriteSessionConfig, + openTelemetry, + enableOtelMetrics, + enableOtelDebugMetrics, + meterProvider, + metricInterval, + baseHashCode()); } @Override @@ -125,6 +188,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(meterProvider, that.meterProvider) + && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -132,6 +199,7 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE in.defaultReadObject(); this.retryDepsAdapter = new RetryDependenciesAdapter(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); + this.meterProvider = this.openTelemetry.getMeterProvider(); } public static HttpStorageOptions.Builder newBuilder() { @@ -157,6 +225,10 @@ public static class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = HttpStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = HttpStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); + private Boolean enableOtelMetrics = null; + private Boolean enableOtelDebugMetrics = null; + private MeterProvider meterProvider = null; + private Duration metricInterval = Duration.ofSeconds(60); Builder() {} @@ -166,6 +238,10 @@ public static class Builder extends StorageOptions.Builder { this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; this.blobWriteSessionConfig = hso.blobWriteSessionConfig; this.openTelemetry = hso.getOpenTelemetry(); + this.enableOtelMetrics = hso.isEnableOtelMetrics(); + this.enableOtelDebugMetrics = hso.isEnableOtelDebugMetrics(); + this.meterProvider = hso.getMeterProvider(); + this.metricInterval = hso.getMetricInterval(); } @Override @@ -317,6 +393,61 @@ public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) this.openTelemetry = openTelemetry; return this; } + + /** + * Enable or disable OpenTelemetry client metrics. + * + * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { + this.enableOtelDebugMetrics = enableOtelDebugMetrics; + return this; + } + + /** + * Set a custom {@link MeterProvider} for recording client metrics. + * + * @param meterProvider custom MeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setMeterProvider(MeterProvider meterProvider) { + this.meterProvider = meterProvider; + return this; + } + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + @Override + public HttpStorageOptions.Builder setMetricInterval(Duration metricInterval) { + checkArgument( + metricInterval != null && !metricInterval.isNegative() && !metricInterval.isZero(), + "metricInterval must be positive"); + this.metricInterval = metricInterval; + return this; + } } public static final class HttpStorageDefaults extends StorageDefaults { 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..df1a505a9bf4 --- /dev/null +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageMetricsConfig.java @@ -0,0 +1,87 @@ +/* + * 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.function.Function; + +final class StorageMetricsConfig { + static final String SYS_PROP_ENABLE_OTEL_METRICS = "com.google.cloud.storage.enable_otel_metrics"; + static final String SYS_PROP_ENABLE_OTEL_DEBUG_METRICS = + "com.google.cloud.storage.enable_otel_debug_metrics"; + static final String ENV_ENABLE_OTEL_METRICS_JAVA = "GCP_STORAGE_JAVA_ENABLE_OTEL_METRICS"; + static final String ENV_ENABLE_OTEL_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; + static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; + + private static Function sysPropResolver = System::getProperty; + private static Function envResolver = System::getenv; + + private StorageMetricsConfig() {} + + static boolean isEnableOtelMetrics() { + return isEnableOtelMetrics(sysPropResolver, envResolver); + } + + static boolean isEnableOtelDebugMetrics() { + return isEnableOtelDebugMetrics(sysPropResolver, envResolver); + } + + @VisibleForTesting + static boolean isEnableOtelMetrics( + Function sysProps, Function env) { + String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_METRICS); + if (sysProp != null) { + return Boolean.parseBoolean(sysProp); + } + String javaEnv = env.apply(ENV_ENABLE_OTEL_METRICS_JAVA); + if (javaEnv != null) { + return Boolean.parseBoolean(javaEnv); + } + String fallbackEnv = env.apply(ENV_ENABLE_OTEL_METRICS_FALLBACK); + if (fallbackEnv != null) { + return Boolean.parseBoolean(fallbackEnv); + } + return false; + } + + @VisibleForTesting + static boolean isEnableOtelDebugMetrics( + Function sysProps, Function env) { + String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); + if (sysProp != null) { + return Boolean.parseBoolean(sysProp); + } + String javaEnv = env.apply(ENV_ENABLE_OTEL_DEBUG_METRICS); + if (javaEnv != null) { + return Boolean.parseBoolean(javaEnv); + } + return false; + } + + @VisibleForTesting + static void setResolversForTesting( + Function testSysProps, Function testEnv) { + sysPropResolver = testSysProps != null ? testSysProps : System::getProperty; + envResolver = testEnv != null ? testEnv : System::getenv; + } + + @VisibleForTesting + static void resetResolversForTesting() { + sysPropResolver = System::getProperty; + 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..791c1b47f737 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 @@ -33,11 +33,14 @@ 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.time.Duration; import java.util.Locale; import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; public abstract class StorageOptions extends ServiceOptions { @@ -143,6 +146,43 @@ 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.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics); + + /** + * Enable or disable OpenTelemetry debug client metrics. + * + * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setEnableOtelDebugMetrics( + boolean enableOtelDebugMetrics); + + /** + * Set a custom {@link MeterProvider} for recording client metrics. + * + * @param meterProvider custom MeterProvider to use + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setMeterProvider(MeterProvider meterProvider); + + /** + * Set the metric export interval for periodic metric reading. + * + * @param metricInterval interval duration + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract StorageOptions.Builder setMetricInterval(Duration metricInterval); + @Override public abstract StorageOptions build(); } @@ -185,6 +225,38 @@ public static String version() { @BetaApi public abstract OpenTelemetry getOpenTelemetry(); + /** + * Whether OpenTelemetry client metrics are enabled. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract boolean isEnableOtelMetrics(); + + /** + * Whether OpenTelemetry debug client metrics are enabled. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract boolean isEnableOtelDebugMetrics(); + + /** + * The {@link MeterProvider} configured for recording client metrics. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract @Nullable MeterProvider getMeterProvider(); + + /** + * The metric export interval configured for periodic metric reading. + * + * @since 2.50.0 This new api is in preview and is subject to breaking changes. + */ + @BetaApi + public abstract Duration getMetricInterval(); + @SuppressWarnings("unchecked") @Override public abstract StorageOptions.Builder toBuilder(); 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..f7e83d5c2158 --- /dev/null +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageOptionsTest.java @@ -0,0 +1,242 @@ +/* + * 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.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() { + System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS); + System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); + StorageMetricsConfig.resetResolversForTesting(); + } + + @Test + public void defaultState_metricsDisabled() { + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + assertThat(httpOptions.isEnableOtelDebugMetrics()).isFalse(); + assertThat(httpOptions.getMeterProvider()) + .isEqualTo(httpOptions.getOpenTelemetry().getMeterProvider()); + assertThat(httpOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + assertThat(grpcOptions.isEnableOtelDebugMetrics()).isFalse(); + assertThat(grpcOptions.getMeterProvider()) + .isEqualTo(grpcOptions.getOpenTelemetry().getMeterProvider()); + 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 builder_explicitDisablingOverridesGate() { + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + + HttpStorageOptions httpOptions = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); + assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + } + + @Test + public void developmentGate_systemProperty() { + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS, "true"); + + 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 developmentGate_environmentVariableJava() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true"); + StorageMetricsConfig.setResolversForTesting(k -> null, 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 developmentGate_environmentVariableFallback() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_FALLBACK, "true"); + StorageMetricsConfig.setResolversForTesting(k -> null, env::get); + + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isTrue(); + } + + @Test + public void developmentGate_systemPropertyTakesPrecedenceOverEnv() { + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); + Map sysProps = new HashMap<>(); + sysProps.put(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); + StorageMetricsConfig.setResolversForTesting(sysProps::get, env::get); + + HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); + assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + + GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); + assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + } + + @Test + public void setMetricInterval_validation() { + 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)); + } + + @Test + public void serializationRoundTrip_preservesMeterProvider() 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 falls back to openTelemetry.getMeterProvider() after deserialization + assertThat(deserializedHttp.getMeterProvider()).isNotNull(); + assertThat(deserializedHttp.getMeterProvider()) + .isEqualTo(deserializedHttp.getOpenTelemetry().getMeterProvider()); + + 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(); + assertThat(deserializedGrpc.getMeterProvider()).isNotNull(); + assertThat(deserializedGrpc.getMeterProvider()) + .isEqualTo(deserializedGrpc.getOpenTelemetry().getMeterProvider()); + } +} From 07dcbb1a20078c2394b4beacf558cdcd45d93c1d Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 23 Sep 2026 10:56:59 +0000 Subject: [PATCH 20/28] fix(storage): preserve meterProvider equality across toBuilder and serialization [Generated-by: AI] --- .../java/com/google/cloud/storage/GrpcStorageOptions.java | 7 +++---- .../java/com/google/cloud/storage/HttpStorageOptions.java | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) 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 02ac9ef28304..db0a4575b518 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 @@ -211,7 +211,6 @@ GrpcInterceptorProvider getGrpcInterceptorProvider() { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); - this.meterProvider = this.openTelemetry.getMeterProvider(); } /** @@ -490,7 +489,7 @@ public int hashCode() { openTelemetry, enableOtelMetrics, enableOtelDebugMetrics, - meterProvider, + getMeterProvider(), metricInterval, baseHashCode()); } @@ -513,7 +512,7 @@ public boolean equals(Object o) { && Objects.equals(grpcInterceptorProvider, that.grpcInterceptorProvider) && Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig) && Objects.equals(openTelemetry, that.openTelemetry) - && Objects.equals(meterProvider, that.meterProvider) + && Objects.equals(getMeterProvider(), that.getMeterProvider()) && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -586,7 +585,7 @@ public static final class Builder extends StorageOptions.Builder { this.openTelemetry = gso.openTelemetry; this.enableOtelMetrics = gso.isEnableOtelMetrics(); this.enableOtelDebugMetrics = gso.isEnableOtelDebugMetrics(); - this.meterProvider = gso.getMeterProvider(); + this.meterProvider = gso.meterProvider; this.metricInterval = gso.getMetricInterval(); } 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 da131282881d..75d69d4d7b07 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 @@ -171,7 +171,7 @@ public int hashCode() { openTelemetry, enableOtelMetrics, enableOtelDebugMetrics, - meterProvider, + getMeterProvider(), metricInterval, baseHashCode()); } @@ -190,7 +190,7 @@ public boolean equals(Object o) { && Objects.equals(openTelemetry, that.openTelemetry) && enableOtelMetrics == that.enableOtelMetrics && enableOtelDebugMetrics == that.enableOtelDebugMetrics - && Objects.equals(meterProvider, that.meterProvider) + && Objects.equals(getMeterProvider(), that.getMeterProvider()) && Objects.equals(metricInterval, that.metricInterval) && this.baseEquals(that); } @@ -199,7 +199,6 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE in.defaultReadObject(); this.retryDepsAdapter = new RetryDependenciesAdapter(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); - this.meterProvider = this.openTelemetry.getMeterProvider(); } public static HttpStorageOptions.Builder newBuilder() { @@ -240,7 +239,7 @@ public static class Builder extends StorageOptions.Builder { this.openTelemetry = hso.getOpenTelemetry(); this.enableOtelMetrics = hso.isEnableOtelMetrics(); this.enableOtelDebugMetrics = hso.isEnableOtelDebugMetrics(); - this.meterProvider = hso.getMeterProvider(); + this.meterProvider = hso.meterProvider; this.metricInterval = hso.getMetricInterval(); } From 434948f6528de48d151f66acd67dab39ce8dd923 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Wed, 30 Sep 2026 06:43:50 +0000 Subject: [PATCH 21/28] fix(storage): add deserialization guard for metricInterval and update options - In GrpcStorageOptions and HttpStorageOptions: - Add null check in getMetricInterval() defaulting to 60 seconds - Use getMetricInterval() in equals() and hashCode() - Update Javadoc for setMeterProvider to document nullable/default behavior - In StorageOptions: - Update Javadoc for setMeterProvider on Builder - In StorageMetricsConfig: - Make sysPropResolver and envResolver volatile for thread safety - In StorageOptionsTest: - Add unit test verifying deserialization with null metricInterval defaults to 60s and equals/hashCode match - Verify explicit client options take precedence over env vars and sysprops [Generated-by: AI] --- .../cloud/storage/GrpcStorageOptions.java | 10 +-- .../cloud/storage/HttpStorageOptions.java | 10 +-- .../cloud/storage/StorageMetricsConfig.java | 4 +- .../google/cloud/storage/StorageOptions.java | 4 +- .../cloud/storage/StorageOptionsTest.java | 78 +++++++++++++++++-- 5 files changed, 87 insertions(+), 19 deletions(-) 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 db0a4575b518..777500351133 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 @@ -466,7 +466,7 @@ public boolean isEnableOtelDebugMetrics() { @BetaApi @Override public Duration getMetricInterval() { - return metricInterval; + return metricInterval != null ? metricInterval : Duration.ofSeconds(60); } /** @@ -490,7 +490,7 @@ public int hashCode() { enableOtelMetrics, enableOtelDebugMetrics, getMeterProvider(), - metricInterval, + getMetricInterval(), baseHashCode()); } @@ -513,7 +513,7 @@ public boolean equals(Object o) { && Objects.equals(blobWriteSessionConfig, that.blobWriteSessionConfig) && Objects.equals(openTelemetry, that.openTelemetry) && Objects.equals(getMeterProvider(), that.getMeterProvider()) - && Objects.equals(metricInterval, that.metricInterval) + && Objects.equals(getMetricInterval(), that.getMetricInterval()) && this.baseEquals(that); } @@ -848,12 +848,12 @@ public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe /** * Set a custom {@link MeterProvider} for recording client metrics. * - * @param meterProvider custom MeterProvider to use + * @param meterProvider custom MeterProvider to use, or null to use the default * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override - public GrpcStorageOptions.Builder setMeterProvider(MeterProvider meterProvider) { + public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) { this.meterProvider = meterProvider; 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 75d69d4d7b07..8a401e12e3e1 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 @@ -155,7 +155,7 @@ public boolean isEnableOtelDebugMetrics() { @BetaApi @Override public Duration getMetricInterval() { - return metricInterval; + return metricInterval != null ? metricInterval : Duration.ofSeconds(60); } @Override @@ -172,7 +172,7 @@ public int hashCode() { enableOtelMetrics, enableOtelDebugMetrics, getMeterProvider(), - metricInterval, + getMetricInterval(), baseHashCode()); } @@ -191,7 +191,7 @@ public boolean equals(Object o) { && enableOtelMetrics == that.enableOtelMetrics && enableOtelDebugMetrics == that.enableOtelDebugMetrics && Objects.equals(getMeterProvider(), that.getMeterProvider()) - && Objects.equals(metricInterval, that.metricInterval) + && Objects.equals(getMetricInterval(), that.getMetricInterval()) && this.baseEquals(that); } @@ -422,12 +422,12 @@ public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe /** * Set a custom {@link MeterProvider} for recording client metrics. * - * @param meterProvider custom MeterProvider to use + * @param meterProvider custom MeterProvider to use, or null to use the default * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override - public HttpStorageOptions.Builder setMeterProvider(MeterProvider meterProvider) { + public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) { this.meterProvider = meterProvider; 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 index df1a505a9bf4..2e7e5859a9a2 100644 --- 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 @@ -27,8 +27,8 @@ final class StorageMetricsConfig { static final String ENV_ENABLE_OTEL_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; - private static Function sysPropResolver = System::getProperty; - private static Function envResolver = System::getenv; + private static volatile Function sysPropResolver = System::getProperty; + private static volatile Function envResolver = System::getenv; private StorageMetricsConfig() {} 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 791c1b47f737..f578109b4f0d 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 @@ -168,11 +168,11 @@ public abstract StorageOptions.Builder setEnableOtelDebugMetrics( /** * Set a custom {@link MeterProvider} for recording client metrics. * - * @param meterProvider custom MeterProvider to use + * @param meterProvider custom MeterProvider to use, or null to use the default * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract StorageOptions.Builder setMeterProvider(MeterProvider meterProvider); + public abstract StorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider); /** * Set the metric export interval for periodic metric reading. 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 index f7e83d5c2158..46c934fda911 100644 --- 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 @@ -25,6 +25,7 @@ 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; @@ -100,14 +101,28 @@ public void builder_explicitEnabling() { } @Test - public void builder_explicitDisablingOverridesGate() { + public void builder_explicitOptionsTakePrecedenceOverGate() { + // When sysprop and env are set to true, explicit builder false takes precedence System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + Map env = new HashMap<>(); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); + StorageMetricsConfig.setResolversForTesting(System::getProperty, env::get); - HttpStorageOptions httpOptions = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); - assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + HttpStorageOptions httpDisabled = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); + assertThat(httpDisabled.isEnableOtelMetrics()).isFalse(); - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + GrpcStorageOptions grpcDisabled = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); + assertThat(grpcDisabled.isEnableOtelMetrics()).isFalse(); + + // When sysprop and env are set to false, explicit builder true takes precedence + System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false"); + + HttpStorageOptions httpEnabled = HttpStorageOptions.http().setEnableOtelMetrics(true).build(); + assertThat(httpEnabled.isEnableOtelMetrics()).isTrue(); + + GrpcStorageOptions grpcEnabled = GrpcStorageOptions.grpc().setEnableOtelMetrics(true).build(); + assertThat(grpcEnabled.isEnableOtelMetrics()).isTrue(); } @Test @@ -239,4 +254,57 @@ public void serializationRoundTrip_preservesMeterProvider() throws Exception { assertThat(deserializedGrpc.getMeterProvider()) .isEqualTo(deserializedGrpc.getOpenTelemetry().getMeterProvider()); } + + @Test + public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throws Exception { + Field httpBuilderField = HttpStorageOptions.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 = GrpcStorageOptions.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()); + } } From 327b558fe25ca6bc126f266eb4ffc6159fb11aab Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Thu, 8 Oct 2026 06:59:01 +0000 Subject: [PATCH 22/28] fix(storage): address PR review comments for otel metrics config [Generated-by: AI] --- .../cloud/storage/GrpcStorageOptions.java | 34 ++- .../cloud/storage/HttpStorageOptions.java | 33 ++- .../cloud/storage/StorageMetricsConfig.java | 84 ++++--- .../google/cloud/storage/StorageOptions.java | 4 + .../cloud/storage/StorageOptionsTest.java | 208 ++++++++++++++---- 5 files changed, 270 insertions(+), 93 deletions(-) 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 777500351133..19f075a9905e 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 @@ -133,6 +133,7 @@ public final class GrpcStorageOptions extends StorageOptions implements Retrying.RetryingDependencies { private static final long serialVersionUID = -4499446543857945349L; + private static final Logger LOGGER = Logger.getLogger(GrpcStorageOptions.class.getName()); private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); private static final String DEFAULT_HOST = "https://storage.googleapis.com"; @@ -142,7 +143,7 @@ public final class GrpcStorageOptions extends StorageOptions System.getProperty("com.google.cloud.storage.grpc.bound_token", "false")); private final GrpcRetryAlgorithmManager retryAlgorithmManager; - private final Duration terminationAwaitDuration; + private final java.time.Duration terminationAwaitDuration; private final boolean attemptDirectPath; private final boolean enableGrpcClientMetrics; @@ -152,8 +153,11 @@ public final class GrpcStorageOptions extends StorageOptions private transient OpenTelemetry openTelemetry; 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 GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -171,16 +175,20 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.grpcInterceptorProvider = builder.grpcInterceptorProvider; this.blobWriteSessionConfig = builder.blobWriteSessionConfig; this.openTelemetry = builder.openTelemetry; + this.rawEnableOtelMetrics = builder.enableOtelMetrics; + this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics; this.enableOtelMetrics = - builder.enableOtelMetrics != null - ? builder.enableOtelMetrics - : StorageMetricsConfig.isEnableOtelMetrics(); + StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); this.enableOtelDebugMetrics = - builder.enableOtelDebugMetrics != null - ? builder.enableOtelDebugMetrics - : StorageMetricsConfig.isEnableOtelDebugMetrics(); + StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics); this.meterProvider = builder.meterProvider; + this.customMeterProviderConfigured = builder.meterProvider != null; this.metricInterval = builder.metricInterval; + if (this.meterProvider == null && 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"); + } } @Override @@ -211,6 +219,10 @@ GrpcInterceptorProvider getGrpcInterceptorProvider() { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); + if (this.customMeterProviderConfigured || this.meterProvider != null) { + LOGGER.warning( + "Custom MeterProvider is transient and cannot be serialized across workers; falling back to default provider."); + } } /** @@ -583,8 +595,8 @@ public static final class Builder extends StorageOptions.Builder { this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; this.openTelemetry = gso.openTelemetry; - this.enableOtelMetrics = gso.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = gso.isEnableOtelDebugMetrics(); + this.enableOtelMetrics = gso.rawEnableOtelMetrics; + this.enableOtelDebugMetrics = gso.rawEnableOtelDebugMetrics; this.meterProvider = gso.meterProvider; this.metricInterval = gso.getMetricInterval(); } @@ -861,6 +873,10 @@ public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter /** * 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)}. + * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ 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 8a401e12e3e1..114a4c865f38 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 @@ -50,6 +50,7 @@ import java.time.Duration; import java.util.Objects; import java.util.Set; +import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -61,6 +62,7 @@ public class HttpStorageOptions extends StorageOptions { private static final long serialVersionUID = -5302637952911052045L; + private static final Logger LOGGER = Logger.getLogger(HttpStorageOptions.class.getName()); private static final String API_SHORT_NAME = "Storage"; private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); @@ -71,8 +73,11 @@ public class HttpStorageOptions extends StorageOptions { private transient OpenTelemetry openTelemetry; 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 HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -83,16 +88,20 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { retryDepsAdapter = new RetryDependenciesAdapter(); blobWriteSessionConfig = builder.blobWriteSessionConfig; openTelemetry = builder.openTelemetry; + this.rawEnableOtelMetrics = builder.enableOtelMetrics; + this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics; this.enableOtelMetrics = - builder.enableOtelMetrics != null - ? builder.enableOtelMetrics - : StorageMetricsConfig.isEnableOtelMetrics(); + StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); this.enableOtelDebugMetrics = - builder.enableOtelDebugMetrics != null - ? builder.enableOtelDebugMetrics - : StorageMetricsConfig.isEnableOtelDebugMetrics(); + StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics); this.meterProvider = builder.meterProvider; + this.customMeterProviderConfigured = builder.meterProvider != null; this.metricInterval = builder.metricInterval; + if (this.meterProvider == null && 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"); + } } @Override @@ -199,6 +208,10 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE in.defaultReadObject(); this.retryDepsAdapter = new RetryDependenciesAdapter(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); + if (this.customMeterProviderConfigured || this.meterProvider != null) { + LOGGER.warning( + "Custom MeterProvider is transient and cannot be serialized across workers; falling back to default provider."); + } } public static HttpStorageOptions.Builder newBuilder() { @@ -237,8 +250,8 @@ public static class Builder extends StorageOptions.Builder { this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; this.blobWriteSessionConfig = hso.blobWriteSessionConfig; this.openTelemetry = hso.getOpenTelemetry(); - this.enableOtelMetrics = hso.isEnableOtelMetrics(); - this.enableOtelDebugMetrics = hso.isEnableOtelDebugMetrics(); + this.enableOtelMetrics = hso.rawEnableOtelMetrics; + this.enableOtelDebugMetrics = hso.rawEnableOtelDebugMetrics; this.meterProvider = hso.meterProvider; this.metricInterval = hso.getMetricInterval(); } @@ -435,6 +448,10 @@ public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter /** * 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)}. + * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ 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 index 2e7e5859a9a2..1fbc3801d903 100644 --- 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 @@ -17,71 +17,93 @@ 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 { - static final String SYS_PROP_ENABLE_OTEL_METRICS = "com.google.cloud.storage.enable_otel_metrics"; - static final String SYS_PROP_ENABLE_OTEL_DEBUG_METRICS = - "com.google.cloud.storage.enable_otel_debug_metrics"; + 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_METRICS_FALLBACK = "GCP_STORAGE_ENABLE_OTEL_METRICS"; static final String ENV_ENABLE_OTEL_DEBUG_METRICS = "GCP_STORAGE_JAVA_ENABLE_OTEL_DEBUG_METRICS"; - private static volatile Function sysPropResolver = System::getProperty; private static volatile Function envResolver = System::getenv; private StorageMetricsConfig() {} static boolean isEnableOtelMetrics() { - return isEnableOtelMetrics(sysPropResolver, envResolver); + return isEnableOtelMetrics(null); } static boolean isEnableOtelDebugMetrics() { - return isEnableOtelDebugMetrics(sysPropResolver, envResolver); + 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 sysProps, Function env) { - String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_METRICS); - if (sysProp != null) { - return Boolean.parseBoolean(sysProp); - } + Function env, @Nullable Boolean builderSetting) { String javaEnv = env.apply(ENV_ENABLE_OTEL_METRICS_JAVA); - if (javaEnv != null) { - return Boolean.parseBoolean(javaEnv); - } - String fallbackEnv = env.apply(ENV_ENABLE_OTEL_METRICS_FALLBACK); - if (fallbackEnv != null) { - return Boolean.parseBoolean(fallbackEnv); + Boolean parsed = parseBooleanValue(javaEnv, ENV_ENABLE_OTEL_METRICS_JAVA); + if (parsed != null) { + return parsed; } - return false; + return builderSetting != null ? builderSetting : false; } @VisibleForTesting static boolean isEnableOtelDebugMetrics( - Function sysProps, Function env) { - String sysProp = sysProps.apply(SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); - if (sysProp != null) { - return Boolean.parseBoolean(sysProp); - } + Function env, @Nullable Boolean builderSetting) { String javaEnv = env.apply(ENV_ENABLE_OTEL_DEBUG_METRICS); - if (javaEnv != null) { - return Boolean.parseBoolean(javaEnv); + Boolean parsed = parseBooleanValue(javaEnv, ENV_ENABLE_OTEL_DEBUG_METRICS); + if (parsed != null) { + return parsed; + } + return builderSetting != null ? builderSetting : 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; } - return false; } @VisibleForTesting - static void setResolversForTesting( - Function testSysProps, Function testEnv) { - sysPropResolver = testSysProps != null ? testSysProps : System::getProperty; + static void setEnvResolverForTesting(Function testEnv) { envResolver = testEnv != null ? testEnv : System::getenv; } @VisibleForTesting static void resetResolversForTesting() { - sysPropResolver = System::getProperty; 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 f578109b4f0d..82ab77a5238b 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 @@ -177,6 +177,10 @@ public abstract StorageOptions.Builder setEnableOtelDebugMetrics( /** * 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)}. + * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. */ 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 index 46c934fda911..f56c707ae745 100644 --- 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 @@ -36,8 +36,6 @@ public final class StorageOptionsTest { @After public void tearDown() { - System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS); - System.clearProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS); StorageMetricsConfig.resetResolversForTesting(); } @@ -101,90 +99,169 @@ public void builder_explicitEnabling() { } @Test - public void builder_explicitOptionsTakePrecedenceOverGate() { - // When sysprop and env are set to true, explicit builder false takes precedence - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); + public void precedence_envTakesPrecedenceOverBuilder() { Map env = new HashMap<>(); env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); - StorageMetricsConfig.setResolversForTesting(System::getProperty, env::get); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true"); + StorageMetricsConfig.setEnvResolverForTesting(env::get); - HttpStorageOptions httpDisabled = HttpStorageOptions.http().setEnableOtelMetrics(false).build(); - assertThat(httpDisabled.isEnableOtelMetrics()).isFalse(); + HttpStorageOptions httpDisabled = + HttpStorageOptions.http() + .setEnableOtelMetrics(false) + .setEnableOtelDebugMetrics(false) + .build(); + assertThat(httpDisabled.isEnableOtelMetrics()).isTrue(); + assertThat(httpDisabled.isEnableOtelDebugMetrics()).isTrue(); - GrpcStorageOptions grpcDisabled = GrpcStorageOptions.grpc().setEnableOtelMetrics(false).build(); - assertThat(grpcDisabled.isEnableOtelMetrics()).isFalse(); + GrpcStorageOptions grpcDisabled = + GrpcStorageOptions.grpc() + .setEnableOtelMetrics(false) + .setEnableOtelDebugMetrics(false) + .build(); + assertThat(grpcDisabled.isEnableOtelMetrics()).isTrue(); + assertThat(grpcDisabled.isEnableOtelDebugMetrics()).isTrue(); - // When sysprop and env are set to false, explicit builder true takes precedence - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); 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()).isFalse(); + assertThat(httpEnabled.isEnableOtelDebugMetrics()).isFalse(); - HttpStorageOptions httpEnabled = HttpStorageOptions.http().setEnableOtelMetrics(true).build(); + GrpcStorageOptions grpcEnabled = + GrpcStorageOptions.grpc() + .setEnableOtelMetrics(true) + .setEnableOtelDebugMetrics(true) + .build(); + assertThat(grpcEnabled.isEnableOtelMetrics()).isFalse(); + assertThat(grpcEnabled.isEnableOtelDebugMetrics()).isFalse(); + } + + @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).build(); + 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 developmentGate_systemProperty() { - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "true"); - System.setProperty(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_DEBUG_METRICS, "true"); + 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); + assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isTrue(); + } - HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); - assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); - assertThat(httpOptions.isEnableOtelDebugMetrics()).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); + assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isFalse(); + } - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isTrue(); - assertThat(grpcOptions.isEnableOtelDebugMetrics()).isTrue(); + 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 + 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(); + } + + assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull(); } @Test - public void developmentGate_environmentVariableJava() { + 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.setResolversForTesting(k -> null, env::get); + 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(); - } - - @Test - public void developmentGate_environmentVariableFallback() { - Map env = new HashMap<>(); - env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_FALLBACK, "true"); - StorageMetricsConfig.setResolversForTesting(k -> null, env::get); - HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); - assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); - - GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isTrue(); + env.clear(); + GrpcStorageOptions rebuiltGrpc = grpcOptions.toBuilder().build(); + assertThat(rebuiltGrpc.isEnableOtelMetrics()).isFalse(); + assertThat(rebuiltGrpc.isEnableOtelDebugMetrics()).isFalse(); } @Test - public void developmentGate_systemPropertyTakesPrecedenceOverEnv() { + public void environmentVariableJava() { Map env = new HashMap<>(); env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "true"); - Map sysProps = new HashMap<>(); - sysProps.put(StorageMetricsConfig.SYS_PROP_ENABLE_OTEL_METRICS, "false"); - StorageMetricsConfig.setResolversForTesting(sysProps::get, env::get); + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_DEBUG_METRICS, "true"); + StorageMetricsConfig.setEnvResolverForTesting(env::get); HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); - assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); + assertThat(httpOptions.isEnableOtelMetrics()).isTrue(); + assertThat(httpOptions.isEnableOtelDebugMetrics()).isTrue(); GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc().build(); - assertThat(grpcOptions.isEnableOtelMetrics()).isFalse(); + 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))); @@ -202,10 +279,49 @@ public void setMetricInterval_validation() { () -> 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)); } @Test - public void serializationRoundTrip_preservesMeterProvider() throws Exception { + public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws Exception { MeterProvider customMeterProvider = SdkMeterProvider.builder().build(); HttpStorageOptions httpOptions = HttpStorageOptions.http() @@ -226,8 +342,9 @@ public void serializationRoundTrip_preservesMeterProvider() throws Exception { assertThat(deserializedHttp).isNotNull(); assertThat(deserializedHttp.isEnableOtelMetrics()).isTrue(); - // Transient meterProvider falls back to openTelemetry.getMeterProvider() after deserialization + // Transient meterProvider does not carry over; falls back to openTelemetry.getMeterProvider() without NPE assertThat(deserializedHttp.getMeterProvider()).isNotNull(); + assertThat(deserializedHttp.getMeterProvider()).isNotSameInstanceAs(customMeterProvider); assertThat(deserializedHttp.getMeterProvider()) .isEqualTo(deserializedHttp.getOpenTelemetry().getMeterProvider()); @@ -251,6 +368,7 @@ public void serializationRoundTrip_preservesMeterProvider() throws Exception { assertThat(deserializedGrpc).isNotNull(); assertThat(deserializedGrpc.isEnableOtelMetrics()).isTrue(); assertThat(deserializedGrpc.getMeterProvider()).isNotNull(); + assertThat(deserializedGrpc.getMeterProvider()).isNotSameInstanceAs(customMeterProvider); assertThat(deserializedGrpc.getMeterProvider()) .isEqualTo(deserializedGrpc.getOpenTelemetry().getMeterProvider()); } From 517e61f4fb1423137852d50423f0169caf70d543 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Thu, 8 Oct 2026 07:22:12 +0000 Subject: [PATCH 23/28] style(storage): format StorageOptions classes with fmt-maven-plugin Format StorageOptions, GrpcStorageOptions, HttpStorageOptions, and StorageOptionsTest using com.spotify.fmt:fmt-maven-plugin:2.25:format under JDK 17 to resolve CI lint check failure. [Generated-by: AI] --- .../com/google/cloud/storage/GrpcStorageOptions.java | 9 ++++----- .../com/google/cloud/storage/HttpStorageOptions.java | 9 ++++----- .../java/com/google/cloud/storage/StorageOptions.java | 6 +++--- .../com/google/cloud/storage/StorageOptionsTest.java | 3 ++- 4 files changed, 13 insertions(+), 14 deletions(-) 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 19f075a9905e..1589b1b25cd6 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 @@ -177,8 +177,7 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.openTelemetry = builder.openTelemetry; this.rawEnableOtelMetrics = builder.enableOtelMetrics; this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics; - this.enableOtelMetrics = - StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); + this.enableOtelMetrics = StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); this.enableOtelDebugMetrics = StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics); this.meterProvider = builder.meterProvider; @@ -873,9 +872,9 @@ public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter /** * 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)}. + *

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)}. * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. 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 114a4c865f38..3dbde5029d5b 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 @@ -90,8 +90,7 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { openTelemetry = builder.openTelemetry; this.rawEnableOtelMetrics = builder.enableOtelMetrics; this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics; - this.enableOtelMetrics = - StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); + this.enableOtelMetrics = StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); this.enableOtelDebugMetrics = StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics); this.meterProvider = builder.meterProvider; @@ -448,9 +447,9 @@ public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter /** * 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)}. + *

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)}. * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. 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 82ab77a5238b..9de103e65f5f 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 @@ -177,9 +177,9 @@ public abstract StorageOptions.Builder setEnableOtelDebugMetrics( /** * 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)}. + *

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)}. * * @param metricInterval interval duration * @since 2.50.0 This new api is in preview and is subject to breaking changes. 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 index f56c707ae745..b00710c08456 100644 --- 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 @@ -342,7 +342,8 @@ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws assertThat(deserializedHttp).isNotNull(); assertThat(deserializedHttp.isEnableOtelMetrics()).isTrue(); - // Transient meterProvider does not carry over; falls back to openTelemetry.getMeterProvider() without NPE + // Transient meterProvider does not carry over; falls back to openTelemetry.getMeterProvider() + // without NPE assertThat(deserializedHttp.getMeterProvider()).isNotNull(); assertThat(deserializedHttp.getMeterProvider()).isNotSameInstanceAs(customMeterProvider); assertThat(deserializedHttp.getMeterProvider()) From 7f624a23c72f7887d0732a0275a19d8aaff0bc97 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Thu, 15 Oct 2026 06:25:31 +0000 Subject: [PATCH 24/28] fix(storage): address review comments on otel metrics config [Generated-by: AI] --- .../cloud/storage/GrpcStorageOptions.java | 115 ++++-------------- .../cloud/storage/HttpStorageOptions.java | 96 ++------------- .../cloud/storage/StorageMetricsConfig.java | 10 +- .../google/cloud/storage/StorageOptions.java | 114 ++++++++++++++--- .../storage/StorageMetricsConfigTest.java | 111 +++++++++++++++++ .../cloud/storage/StorageOptionsTest.java | 75 +++++++----- 6 files changed, 295 insertions(+), 226 deletions(-) create mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/StorageMetricsConfigTest.java 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 1589b1b25cd6..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 @@ -133,7 +133,6 @@ public final class GrpcStorageOptions extends StorageOptions implements Retrying.RetryingDependencies { private static final long serialVersionUID = -4499446543857945349L; - private static final Logger LOGGER = Logger.getLogger(GrpcStorageOptions.class.getName()); private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); private static final String DEFAULT_HOST = "https://storage.googleapis.com"; @@ -151,13 +150,6 @@ public final class GrpcStorageOptions extends StorageOptions private final GrpcInterceptorProvider grpcInterceptorProvider; private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; - 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 GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -175,19 +167,6 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults) this.grpcInterceptorProvider = builder.grpcInterceptorProvider; this.blobWriteSessionConfig = builder.blobWriteSessionConfig; this.openTelemetry = builder.openTelemetry; - 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.meterProvider != null; - this.metricInterval = builder.metricInterval; - if (this.meterProvider == null && 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"); - } } @Override @@ -201,7 +180,7 @@ GrpcRetryAlgorithmManager getRetryAlgorithmManager() { } @InternalApi - Duration getTerminationAwaitDuration() { + java.time.Duration getTerminationAwaitDuration() { return terminationAwaitDuration; } @@ -218,10 +197,6 @@ GrpcInterceptorProvider getGrpcInterceptorProvider() { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); - if (this.customMeterProviderConfigured || this.meterProvider != null) { - LOGGER.warning( - "Custom MeterProvider is transient and cannot be serialized across workers; falling back to default provider."); - } } /** @@ -390,9 +365,9 @@ private Tuple> resolveSettingsAndOpts() throw // seconds. // To allow read streams to have longer lifespans, crank up their timeouts, instead rely // on idleTimeout below. - .setLogicalTimeout(Duration.ofDays(28)) + .setLogicalTimeout(java.time.Duration.ofDays(28)) .build(); - Duration totalTimeout = baseRetrySettings.getTotalTimeoutDuration(); + java.time.Duration totalTimeout = baseRetrySettings.getTotalTimeoutDuration(); // retries for unary methods are generally handled at a different level, except // StartResumableWrite @@ -441,45 +416,6 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelMetrics() { - return enableOtelMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelDebugMetrics() { - return enableOtelDebugMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public @Nullable MeterProvider getMeterProvider() { - if (meterProvider == null && openTelemetry != null) { - return openTelemetry.getMeterProvider(); - } - return meterProvider; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public Duration getMetricInterval() { - return metricInterval != null ? metricInterval : Duration.ofSeconds(60); - } - /** * @since 2.14.0 */ @@ -566,7 +502,7 @@ protected boolean shouldRefreshService(Storage cachedService) { public static final class Builder extends StorageOptions.Builder { private StorageRetryStrategy storageRetryStrategy; - private Duration terminationAwaitDuration; + private java.time.Duration terminationAwaitDuration; private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath(); private boolean enableGrpcClientMetrics = GrpcStorageDefaults.INSTANCE.isEnableGrpcClientMetrics(); @@ -575,10 +511,6 @@ public static final class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = GrpcStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); - private Boolean enableOtelMetrics = null; - private Boolean enableOtelDebugMetrics = null; - private MeterProvider meterProvider = null; - private Duration metricInterval = Duration.ofSeconds(60); private boolean grpcMetricsManuallyEnabled = false; @@ -594,16 +526,13 @@ public static final class Builder extends StorageOptions.Builder { this.grpcInterceptorProvider = gso.grpcInterceptorProvider; this.blobWriteSessionConfig = gso.blobWriteSessionConfig; this.openTelemetry = gso.openTelemetry; - this.enableOtelMetrics = gso.rawEnableOtelMetrics; - this.enableOtelDebugMetrics = gso.rawEnableOtelDebugMetrics; - this.meterProvider = gso.meterProvider; - this.metricInterval = gso.getMetricInterval(); } /** - * This method is obsolete. Use {@link #setTerminationAwaitJavaTimeDuration(Duration)} instead. + * This method is obsolete. Use {@link #setTerminationAwaitJavaTimeDuration(java.time.Duration)} + * instead. */ - @ObsoleteApi("Use setTerminationAwaitJavaTimeDuration(Duration) instead") + @ObsoleteApi("Use setTerminationAwaitJavaTimeDuration(java.time.Duration) instead") public Builder setTerminationAwaitDuration(org.threeten.bp.Duration terminationAwaitDuration) { return setTerminationAwaitJavaTimeDuration(toJavaTimeDuration(terminationAwaitDuration)); } @@ -616,7 +545,8 @@ public Builder setTerminationAwaitDuration(org.threeten.bp.Duration terminationA * @return the builder * @since 2.14.0 */ - public Builder setTerminationAwaitJavaTimeDuration(Duration terminationAwaitDuration) { + public Builder setTerminationAwaitJavaTimeDuration( + java.time.Duration terminationAwaitDuration) { this.terminationAwaitDuration = requireNonNull(terminationAwaitDuration, "terminationAwaitDuration must be non null"); return this; @@ -827,6 +757,7 @@ 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; } @@ -834,12 +765,12 @@ public GrpcStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) * Enable or disable OpenTelemetry client metrics. * * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public GrpcStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) { - this.enableOtelMetrics = enableOtelMetrics; + super.setEnableOtelMetrics(enableOtelMetrics); return this; } @@ -847,12 +778,12 @@ public GrpcStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics * Enable or disable OpenTelemetry debug client metrics. * * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { - this.enableOtelDebugMetrics = enableOtelDebugMetrics; + super.setEnableOtelDebugMetrics(enableOtelDebugMetrics); return this; } @@ -860,12 +791,12 @@ public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe * Set a custom {@link MeterProvider} for recording client metrics. * * @param meterProvider custom MeterProvider to use, or null to use the default - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @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) { - this.meterProvider = meterProvider; + super.setMeterProvider(meterProvider); return this; } @@ -874,18 +805,16 @@ public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter * *

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)}. + * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit + * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}. * * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public GrpcStorageOptions.Builder setMetricInterval(Duration metricInterval) { - checkArgument( - metricInterval != null && !metricInterval.isNegative() && !metricInterval.isZero(), - "metricInterval must be positive"); - this.metricInterval = metricInterval; + super.setMetricInterval(metricInterval); return this; } @@ -956,8 +885,8 @@ public org.threeten.bp.Duration getTerminationAwaitDuration() { /** * @since 2.14.0 */ - public Duration getTerminationAwaitDurationJavaTime() { - return Duration.ofMinutes(1); + public java.time.Duration getTerminationAwaitDurationJavaTime() { + return java.time.Duration.ofMinutes(1); } /** 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 3dbde5029d5b..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 @@ -50,7 +50,6 @@ import java.time.Duration; import java.util.Objects; import java.util.Set; -import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; @@ -62,7 +61,6 @@ public class HttpStorageOptions extends StorageOptions { private static final long serialVersionUID = -5302637952911052045L; - private static final Logger LOGGER = Logger.getLogger(HttpStorageOptions.class.getName()); private static final String API_SHORT_NAME = "Storage"; private static final String GCS_SCOPE = "https://www.googleapis.com/auth/devstorage.full_control"; private static final Set SCOPES = ImmutableSet.of(GCS_SCOPE); @@ -71,13 +69,6 @@ public class HttpStorageOptions extends StorageOptions { private final BlobWriteSessionConfig blobWriteSessionConfig; private transient OpenTelemetry openTelemetry; - 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 HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { super(builder, serviceDefaults); @@ -88,19 +79,6 @@ private HttpStorageOptions(Builder builder, StorageDefaults serviceDefaults) { retryDepsAdapter = new RetryDependenciesAdapter(); blobWriteSessionConfig = builder.blobWriteSessionConfig; openTelemetry = builder.openTelemetry; - 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.meterProvider != null; - this.metricInterval = builder.metricInterval; - if (this.meterProvider == null && 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"); - } } @Override @@ -127,45 +105,6 @@ public OpenTelemetry getOpenTelemetry() { return openTelemetry; } - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelMetrics() { - return enableOtelMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public boolean isEnableOtelDebugMetrics() { - return enableOtelDebugMetrics; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public @Nullable MeterProvider getMeterProvider() { - if (meterProvider == null && openTelemetry != null) { - return openTelemetry.getMeterProvider(); - } - return meterProvider; - } - - /** - * @since 2.50.0 This new api is in preview and is subject to breaking changes. - */ - @BetaApi - @Override - public Duration getMetricInterval() { - return metricInterval != null ? metricInterval : Duration.ofSeconds(60); - } - @Override public HttpStorageOptions.Builder toBuilder() { return new HttpStorageOptions.Builder(this); @@ -207,10 +146,6 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE in.defaultReadObject(); this.retryDepsAdapter = new RetryDependenciesAdapter(); this.openTelemetry = HttpStorageOptions.getDefaultInstance().getOpenTelemetry(); - if (this.customMeterProviderConfigured || this.meterProvider != null) { - LOGGER.warning( - "Custom MeterProvider is transient and cannot be serialized across workers; falling back to default provider."); - } } public static HttpStorageOptions.Builder newBuilder() { @@ -236,10 +171,6 @@ public static class Builder extends StorageOptions.Builder { private BlobWriteSessionConfig blobWriteSessionConfig = HttpStorageDefaults.INSTANCE.getDefaultStorageWriterConfig(); private OpenTelemetry openTelemetry = HttpStorageDefaults.INSTANCE.getDefaultOpenTelemetry(); - private Boolean enableOtelMetrics = null; - private Boolean enableOtelDebugMetrics = null; - private MeterProvider meterProvider = null; - private Duration metricInterval = Duration.ofSeconds(60); Builder() {} @@ -249,10 +180,6 @@ public static class Builder extends StorageOptions.Builder { this.storageRetryStrategy = hso.retryAlgorithmManager.retryStrategy; this.blobWriteSessionConfig = hso.blobWriteSessionConfig; this.openTelemetry = hso.getOpenTelemetry(); - this.enableOtelMetrics = hso.rawEnableOtelMetrics; - this.enableOtelDebugMetrics = hso.rawEnableOtelDebugMetrics; - this.meterProvider = hso.meterProvider; - this.metricInterval = hso.getMetricInterval(); } @Override @@ -402,6 +329,7 @@ public HttpStorageOptions build() { public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) { requireNonNull(openTelemetry, "openTelemetry must be non null"); this.openTelemetry = openTelemetry; + this.customOpenTelemetryConfigured = true; return this; } @@ -409,12 +337,12 @@ public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) * Enable or disable OpenTelemetry client metrics. * * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public HttpStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) { - this.enableOtelMetrics = enableOtelMetrics; + super.setEnableOtelMetrics(enableOtelMetrics); return this; } @@ -422,12 +350,12 @@ public HttpStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics * Enable or disable OpenTelemetry debug client metrics. * * @param enableOtelDebugMetrics whether OpenTelemetry debug client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { - this.enableOtelDebugMetrics = enableOtelDebugMetrics; + super.setEnableOtelDebugMetrics(enableOtelDebugMetrics); return this; } @@ -435,12 +363,12 @@ public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe * Set a custom {@link MeterProvider} for recording client metrics. * * @param meterProvider custom MeterProvider to use, or null to use the default - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @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) { - this.meterProvider = meterProvider; + super.setMeterProvider(meterProvider); return this; } @@ -449,18 +377,16 @@ public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter * *

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)}. + * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit + * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}. * * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override public HttpStorageOptions.Builder setMetricInterval(Duration metricInterval) { - checkArgument( - metricInterval != null && !metricInterval.isNegative() && !metricInterval.isZero(), - "metricInterval must be positive"); - this.metricInterval = 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 index 1fbc3801d903..30be2ba85ef2 100644 --- 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 @@ -51,23 +51,29 @@ static boolean isEnableOtelDebugMetrics(@Nullable Boolean 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 builderSetting != null ? builderSetting : false; + 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 builderSetting != null ? builderSetting : false; + return false; } @Nullable 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 9de103e65f5f..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; @@ -36,9 +38,11 @@ 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; @@ -115,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); @@ -150,49 +167,102 @@ public abstract StorageOptions.Builder setBlobWriteSessionConfig( * Enable or disable OpenTelemetry client metrics. * * @param enableOtelMetrics whether OpenTelemetry client metrics should be enabled - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics); + 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.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract StorageOptions.Builder setEnableOtelDebugMetrics( - boolean enableOtelDebugMetrics); + 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.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract StorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider); + 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)}. + * MeterProvider} is configured via {@link #setMeterProvider(MeterProvider)} or explicit + * OpenTelemetry is configured via {@link #setOpenTelemetry(OpenTelemetry)}. * * @param metricInterval interval duration - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract StorageOptions.Builder setMetricInterval(Duration metricInterval); + 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 {} @@ -232,34 +302,42 @@ public static String version() { /** * Whether OpenTelemetry client metrics are enabled. * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract boolean isEnableOtelMetrics(); + public boolean isEnableOtelMetrics() { + return enableOtelMetrics; + } /** * Whether OpenTelemetry debug client metrics are enabled. * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract boolean isEnableOtelDebugMetrics(); + public boolean isEnableOtelDebugMetrics() { + return enableOtelDebugMetrics; + } /** * The {@link MeterProvider} configured for recording client metrics. * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract @Nullable MeterProvider getMeterProvider(); + public @Nullable MeterProvider getMeterProvider() { + return meterProvider; + } /** * The metric export interval configured for periodic metric reading. * - * @since 2.50.0 This new api is in preview and is subject to breaking changes. + * @since 2.74.0 This new api is in preview and is subject to breaking changes. */ @BetaApi - public abstract Duration getMetricInterval(); + public Duration getMetricInterval() { + return metricInterval != null ? metricInterval : Duration.ofSeconds(60); + } @SuppressWarnings("unchecked") @Override 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 index b00710c08456..a54b9a01dba3 100644 --- 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 @@ -44,15 +44,13 @@ public void defaultState_metricsDisabled() { HttpStorageOptions httpOptions = HttpStorageOptions.http().build(); assertThat(httpOptions.isEnableOtelMetrics()).isFalse(); assertThat(httpOptions.isEnableOtelDebugMetrics()).isFalse(); - assertThat(httpOptions.getMeterProvider()) - .isEqualTo(httpOptions.getOpenTelemetry().getMeterProvider()); + 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()) - .isEqualTo(grpcOptions.getOpenTelemetry().getMeterProvider()); + assertThat(grpcOptions.getMeterProvider()).isNull(); assertThat(grpcOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); } @@ -99,7 +97,7 @@ public void builder_explicitEnabling() { } @Test - public void precedence_envTakesPrecedenceOverBuilder() { + 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"); @@ -110,16 +108,16 @@ public void precedence_envTakesPrecedenceOverBuilder() { .setEnableOtelMetrics(false) .setEnableOtelDebugMetrics(false) .build(); - assertThat(httpDisabled.isEnableOtelMetrics()).isTrue(); - assertThat(httpDisabled.isEnableOtelDebugMetrics()).isTrue(); + assertThat(httpDisabled.isEnableOtelMetrics()).isFalse(); + assertThat(httpDisabled.isEnableOtelDebugMetrics()).isFalse(); GrpcStorageOptions grpcDisabled = GrpcStorageOptions.grpc() .setEnableOtelMetrics(false) .setEnableOtelDebugMetrics(false) .build(); - assertThat(grpcDisabled.isEnableOtelMetrics()).isTrue(); - assertThat(grpcDisabled.isEnableOtelDebugMetrics()).isTrue(); + 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"); @@ -129,16 +127,16 @@ public void precedence_envTakesPrecedenceOverBuilder() { .setEnableOtelMetrics(true) .setEnableOtelDebugMetrics(true) .build(); - assertThat(httpEnabled.isEnableOtelMetrics()).isFalse(); - assertThat(httpEnabled.isEnableOtelDebugMetrics()).isFalse(); + assertThat(httpEnabled.isEnableOtelMetrics()).isTrue(); + assertThat(httpEnabled.isEnableOtelDebugMetrics()).isTrue(); GrpcStorageOptions grpcEnabled = GrpcStorageOptions.grpc() .setEnableOtelMetrics(true) .setEnableOtelDebugMetrics(true) .build(); - assertThat(grpcEnabled.isEnableOtelMetrics()).isFalse(); - assertThat(grpcEnabled.isEnableOtelDebugMetrics()).isFalse(); + assertThat(grpcEnabled.isEnableOtelMetrics()).isTrue(); + assertThat(grpcEnabled.isEnableOtelDebugMetrics()).isTrue(); } @Test @@ -186,7 +184,11 @@ public void permissiveBooleanParsing() { assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isTrue(); Map env = new HashMap<>(); env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, val); - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isTrue(); + // 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 "}; @@ -194,7 +196,11 @@ public void permissiveBooleanParsing() { assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isFalse(); Map env = new HashMap<>(); env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, val); - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isFalse(); + // 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", ""}; @@ -202,13 +208,16 @@ public void permissiveBooleanParsing() { 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 + // 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(); } @@ -318,6 +327,22 @@ public void setMetricInterval_validation() { .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 @@ -342,12 +367,8 @@ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws assertThat(deserializedHttp).isNotNull(); assertThat(deserializedHttp.isEnableOtelMetrics()).isTrue(); - // Transient meterProvider does not carry over; falls back to openTelemetry.getMeterProvider() - // without NPE - assertThat(deserializedHttp.getMeterProvider()).isNotNull(); - assertThat(deserializedHttp.getMeterProvider()).isNotSameInstanceAs(customMeterProvider); - assertThat(deserializedHttp.getMeterProvider()) - .isEqualTo(deserializedHttp.getOpenTelemetry().getMeterProvider()); + // Transient meterProvider does not carry over; returns null + assertThat(deserializedHttp.getMeterProvider()).isNull(); GrpcStorageOptions grpcOptions = GrpcStorageOptions.grpc() @@ -368,15 +389,13 @@ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws assertThat(deserializedGrpc).isNotNull(); assertThat(deserializedGrpc.isEnableOtelMetrics()).isTrue(); - assertThat(deserializedGrpc.getMeterProvider()).isNotNull(); - assertThat(deserializedGrpc.getMeterProvider()).isNotSameInstanceAs(customMeterProvider); - assertThat(deserializedGrpc.getMeterProvider()) - .isEqualTo(deserializedGrpc.getOpenTelemetry().getMeterProvider()); + // Transient meterProvider does not carry over; returns null + assertThat(deserializedGrpc.getMeterProvider()).isNull(); } @Test public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throws Exception { - Field httpBuilderField = HttpStorageOptions.Builder.class.getDeclaredField("metricInterval"); + Field httpBuilderField = StorageOptions.Builder.class.getDeclaredField("metricInterval"); httpBuilderField.setAccessible(true); HttpStorageOptions.Builder httpBuilder = HttpStorageOptions.http(); httpBuilderField.set(httpBuilder, null); @@ -402,7 +421,7 @@ public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throw assertThat(defaultHttp).isEqualTo(deserializedHttp); assertThat(deserializedHttp.hashCode()).isEqualTo(defaultHttp.hashCode()); - Field grpcBuilderField = GrpcStorageOptions.Builder.class.getDeclaredField("metricInterval"); + Field grpcBuilderField = StorageOptions.Builder.class.getDeclaredField("metricInterval"); grpcBuilderField.setAccessible(true); GrpcStorageOptions.Builder grpcBuilder = GrpcStorageOptions.grpc(); grpcBuilderField.set(grpcBuilder, null); From 4f4f7266572d7e3772a59f8a2afd377adbbb8c25 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 19 Oct 2026 07:47:06 +0000 Subject: [PATCH 25/28] fix(storage): clamp metricInterval on deserialization and resolve review comments Clamp metric export interval to at least 60 seconds when meterProvider is null and no custom OpenTelemetry is configured, preventing invalid sub-60s intervals during deserialization when falling back to default Cloud Monitoring exporter. [Generated-by: AI] --- .../google/cloud/storage/StorageOptions.java | 8 ++- .../cloud/storage/StorageOptionsTest.java | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) 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 2d123041ca20..2b81b87a768b 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 @@ -336,7 +336,13 @@ public boolean isEnableOtelDebugMetrics() { */ @BetaApi public Duration getMetricInterval() { - return metricInterval != null ? metricInterval : Duration.ofSeconds(60); + 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") 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 index a54b9a01dba3..222e82da7dd8 100644 --- 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 @@ -393,6 +393,55 @@ public void serializationRoundTrip_customMeterProviderDoesNotCarryOver() throws assertThat(deserializedGrpc.getMeterProvider()).isNull(); } + @Test + public void serializationRoundTrip_customMeterProviderWithSub60sInterval_clampsToSixtySeconds() + throws Exception { + MeterProvider customMeterProvider = SdkMeterProvider.builder().build(); + HttpStorageOptions httpOptions = + HttpStorageOptions.http() + .setMeterProvider(customMeterProvider) + .setMetricInterval(Duration.ofSeconds(30)) + .build(); + assertThat(httpOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); + + 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.getMeterProvider()).isNull(); + assertThat(deserializedHttp.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); + + GrpcStorageOptions grpcOptions = + GrpcStorageOptions.grpc() + .setMeterProvider(customMeterProvider) + .setMetricInterval(Duration.ofSeconds(30)) + .build(); + assertThat(grpcOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); + + 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.getMeterProvider()).isNull(); + assertThat(deserializedGrpc.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); + } + @Test public void deserializationWithNullMetricInterval_defaultsToSixtySeconds() throws Exception { Field httpBuilderField = StorageOptions.Builder.class.getDeclaredField("metricInterval"); From 9519e62bf75f61ea8981a167782880d126a5c57b Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Mon, 19 Oct 2026 09:58:41 +0000 Subject: [PATCH 26/28] refactor(storage): address PR review comments for client metrics config and tests Deduplicate flag resolution in StorageMetricsConfig by extracting resolveFlag helper, separate multi-behavior tests per go/tott/726, and add input diagnostic messages in looped assertions. [Generated-by: AI] --- .../cloud/storage/StorageMetricsConfig.java | 19 ++--- .../storage/StorageMetricsConfigTest.java | 43 ++++++---- .../cloud/storage/StorageOptionsTest.java | 83 ++++++++++++++----- 3 files changed, 99 insertions(+), 46 deletions(-) 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 index 30be2ba85ef2..923d72d146b8 100644 --- 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 @@ -51,25 +51,22 @@ static boolean isEnableOtelDebugMetrics(@Nullable Boolean 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; + 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(ENV_ENABLE_OTEL_DEBUG_METRICS); - Boolean parsed = parseBooleanValue(javaEnv, ENV_ENABLE_OTEL_DEBUG_METRICS); + String javaEnv = env.apply(envVarName); + Boolean parsed = parseBooleanValue(javaEnv, envVarName); if (parsed != null) { return parsed; } 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 index 5cb4f3946490..46e1f5494b32 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -31,48 +32,54 @@ public void tearDown() { } @Test - public void isEnableOtelMetrics_precedenceOrder() { + public void isEnableOtelMetrics_explicitBuilderSettingTakesPrecedenceOverEnvVar() { 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 + @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(); + } - // 3. When both builder setting and env var are absent/null, default is false - env.clear(); + @Test + public void isEnableOtelMetrics_defaultsToFalseWhenUnset() { + Map env = new HashMap<>(); assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse(); } @Test - public void isEnableOtelDebugMetrics_precedenceOrder() { + public void isEnableOtelDebugMetrics_explicitBuilderSettingTakesPrecedenceOverEnvVar() { 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 + @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(); + } - // 3. When both builder setting and env var are absent/null, default is false - env.clear(); + @Test + public void isEnableOtelDebugMetrics_defaultsToFalseWhenUnset() { + Map env = new HashMap<>(); assertThat(StorageMetricsConfig.isEnableOtelDebugMetrics(env::get, null)).isFalse(); } @@ -80,17 +87,23 @@ public void isEnableOtelDebugMetrics_precedenceOrder() { public void parseBooleanValue_handlesPermissiveValues() { String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "}; for (String val : trueValues) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isTrue(); + 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) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isFalse(); + 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) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isNull(); + assertWithMessage("Failed for input value: '%s'", val) + .that(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")) + .isNull(); } assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull(); 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 index 222e82da7dd8..e6df708367c9 100644 --- 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 @@ -17,6 +17,7 @@ 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; @@ -178,44 +179,76 @@ public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() { } @Test - public void permissiveBooleanParsing() { + public void permissiveBooleanParsing_trueValues() { String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "}; for (String val : trueValues) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isTrue(); + 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 - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isTrue(); + assertWithMessage("Failed for input value: '%s'", val) + .that(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(); + 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) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isFalse(); + 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 - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse(); + assertWithMessage("Failed for input value: '%s'", val) + .that(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(); + 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) { - assertThat(StorageMetricsConfig.parseBooleanValue(val, "TEST_VAR")).isNull(); + 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 - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, true)).isTrue(); - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, false)).isFalse(); + 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 - assertThat(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)).isFalse(); + assertWithMessage("Failed for input value: '%s'", val) + .that(StorageMetricsConfig.isEnableOtelMetrics(env::get, null)) + .isFalse(); } + } - // When both are absent/null, default is false + @Test + public void permissiveBooleanParsing_nullHandled() { Map emptyEnv = new HashMap<>(); assertThat(StorageMetricsConfig.isEnableOtelMetrics(emptyEnv::get, null)).isFalse(); assertThat(StorageMetricsConfig.parseBooleanValue(null, "TEST_VAR")).isNull(); @@ -268,9 +301,7 @@ public void environmentVariableJava() { } @Test - public void setMetricInterval_validation() { - MeterProvider customMeterProvider = SdkMeterProvider.builder().build(); - + public void setMetricInterval_rejectsNullZeroOrNegative() { assertThrows( IllegalArgumentException.class, () -> HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(-1))); @@ -288,7 +319,10 @@ public void setMetricInterval_validation() { () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ZERO)); assertThrows( IllegalArgumentException.class, () -> GrpcStorageOptions.grpc().setMetricInterval(null)); + } + @Test + public void setMetricInterval_rejectsSubSixtySecondsWithoutCustomProvider() { // Reject intervals < 60s when meterProvider == null (SDK-owned Cloud Monitoring exporter) assertThrows( IllegalArgumentException.class, @@ -303,8 +337,10 @@ public void setMetricInterval_validation() { assertThrows( IllegalArgumentException.class, () -> GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(30)).build()); + } - // Accept intervals >= 60s when meterProvider == null + @Test + public void setMetricInterval_acceptsSixtySecondsOrGreaterWithDefaultProvider() { HttpStorageOptions http60 = HttpStorageOptions.http().setMetricInterval(Duration.ofSeconds(60)).build(); assertThat(http60.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); @@ -312,8 +348,12 @@ public void setMetricInterval_validation() { GrpcStorageOptions grpc60 = GrpcStorageOptions.grpc().setMetricInterval(Duration.ofSeconds(60)).build(); assertThat(grpc60.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); + } + + @Test + public void setMetricInterval_acceptsSubSixtySecondsWithCustomMeterProvider() { + MeterProvider customMeterProvider = SdkMeterProvider.builder().build(); - // Accept intervals < 60s when custom meterProvider is configured HttpStorageOptions httpCustom = HttpStorageOptions.http() .setMeterProvider(customMeterProvider) @@ -327,9 +367,12 @@ public void setMetricInterval_validation() { .setMetricInterval(Duration.ofSeconds(30)) .build(); assertThat(grpcCustom.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); + } - // Accept intervals < 60s when explicit openTelemetry is configured + @Test + public void setMetricInterval_acceptsSubSixtySecondsWithCustomOpenTelemetry() { io.opentelemetry.api.OpenTelemetry customOtel = io.opentelemetry.api.OpenTelemetry.noop(); + HttpStorageOptions httpCustomOtel = HttpStorageOptions.http() .setOpenTelemetry(customOtel) From 3d889a73369b7d4690f21c23918b09d6469351b7 Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 07:14:07 +0000 Subject: [PATCH 27/28] fix(storage): address review comments on PR #14484 for metrics options and gating --- .../cloud/storage/GrpcStorageOptions.java | 18 +-- .../cloud/storage/HttpStorageOptions.java | 18 +-- .../google/cloud/storage/StorageOptions.java | 116 ++++++++++++------ .../storage/StorageMetricsConfigTest.java | 60 +++++++++ .../cloud/storage/StorageOptionsTest.java | 71 +++++++++++ 5 files changed, 230 insertions(+), 53 deletions(-) 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 e2415c798154..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 @@ -434,8 +434,8 @@ public int hashCode() { grpcInterceptorProvider, blobWriteSessionConfig, openTelemetry, - enableOtelMetrics, - enableOtelDebugMetrics, + isEnableOtelMetrics(), + isEnableOtelDebugMetrics(), getMeterProvider(), getMetricInterval(), baseHashCode()); @@ -452,8 +452,8 @@ public boolean equals(Object o) { GrpcStorageOptions that = (GrpcStorageOptions) o; return attemptDirectPath == that.attemptDirectPath && enableGrpcClientMetrics == that.enableGrpcClientMetrics - && enableOtelMetrics == that.enableOtelMetrics - && enableOtelDebugMetrics == that.enableOtelDebugMetrics + && isEnableOtelMetrics() == that.isEnableOtelMetrics() + && isEnableOtelDebugMetrics() == that.isEnableOtelDebugMetrics() && Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager) && Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration) && Objects.equals(grpcInterceptorProvider, that.grpcInterceptorProvider) @@ -757,7 +757,7 @@ 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; + setCustomOpenTelemetryConfigured(true); return this; } @@ -765,7 +765,7 @@ public GrpcStorageOptions.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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -778,7 +778,7 @@ public GrpcStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -791,7 +791,7 @@ public GrpcStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -809,7 +809,7 @@ public GrpcStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override 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 ee3e73487464..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 @@ -116,8 +116,8 @@ public int hashCode() { retryAlgorithmManager, blobWriteSessionConfig, openTelemetry, - enableOtelMetrics, - enableOtelDebugMetrics, + isEnableOtelMetrics(), + isEnableOtelDebugMetrics(), getMeterProvider(), getMetricInterval(), baseHashCode()); @@ -135,8 +135,8 @@ 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 + && isEnableOtelMetrics() == that.isEnableOtelMetrics() + && isEnableOtelDebugMetrics() == that.isEnableOtelDebugMetrics() && Objects.equals(getMeterProvider(), that.getMeterProvider()) && Objects.equals(getMetricInterval(), that.getMetricInterval()) && this.baseEquals(that); @@ -329,7 +329,7 @@ public HttpStorageOptions build() { public HttpStorageOptions.Builder setOpenTelemetry(OpenTelemetry openTelemetry) { requireNonNull(openTelemetry, "openTelemetry must be non null"); this.openTelemetry = openTelemetry; - this.customOpenTelemetryConfigured = true; + setCustomOpenTelemetryConfigured(true); return this; } @@ -337,7 +337,7 @@ public HttpStorageOptions.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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -350,7 +350,7 @@ public HttpStorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -363,7 +363,7 @@ public HttpStorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDe * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override @@ -381,7 +381,7 @@ public HttpStorageOptions.Builder setMeterProvider(@Nullable MeterProvider meter * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi @Override 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 2b81b87a768b..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 @@ -119,12 +119,12 @@ 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; + 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); @@ -132,12 +132,40 @@ public abstract static class Builder Builder(StorageOptions options) { super(options); - this.enableOtelMetrics = options.rawEnableOtelMetrics; - this.enableOtelDebugMetrics = options.rawEnableOtelDebugMetrics; - this.meterProvider = options.meterProvider; + this.enableOtelMetrics = options.getRawEnableOtelMetrics(); + this.enableOtelDebugMetrics = options.getRawEnableOtelDebugMetrics(); + this.meterProvider = options.getMeterProvider(); this.metricInterval = options.metricInterval; - this.customMeterProviderConfigured = options.customMeterProviderConfigured; - this.customOpenTelemetryConfigured = options.customOpenTelemetryConfigured; + 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); @@ -167,7 +195,7 @@ public abstract StorageOptions.Builder setBlobWriteSessionConfig( * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) { @@ -179,7 +207,7 @@ public StorageOptions.Builder setEnableOtelMetrics(boolean enableOtelMetrics) { * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public StorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugMetrics) { @@ -191,7 +219,7 @@ public StorageOptions.Builder setEnableOtelDebugMetrics(boolean enableOtelDebugM * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public StorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProvider) { @@ -209,7 +237,7 @@ public StorageOptions.Builder setMeterProvider(@Nullable MeterProvider meterProv * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public StorageOptions.Builder setMetricInterval(Duration metricInterval) { @@ -226,28 +254,30 @@ public StorageOptions.Builder setMetricInterval(Duration metricInterval) { 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; + 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.enableOtelMetrics; - this.rawEnableOtelDebugMetrics = builder.enableOtelDebugMetrics; - this.enableOtelMetrics = StorageMetricsConfig.isEnableOtelMetrics(builder.enableOtelMetrics); + this.rawEnableOtelMetrics = builder.getEnableOtelMetrics(); + this.rawEnableOtelDebugMetrics = builder.getEnableOtelDebugMetrics(); + this.enableOtelMetrics = + StorageMetricsConfig.isEnableOtelMetrics(builder.getEnableOtelMetrics()); this.enableOtelDebugMetrics = - StorageMetricsConfig.isEnableOtelDebugMetrics(builder.enableOtelDebugMetrics); - this.meterProvider = builder.meterProvider; + this.enableOtelMetrics + && StorageMetricsConfig.isEnableOtelDebugMetrics(builder.getEnableOtelDebugMetrics()); + this.meterProvider = builder.getMeterProvider(); this.customMeterProviderConfigured = - builder.customMeterProviderConfigured || builder.meterProvider != null; - this.customOpenTelemetryConfigured = builder.customOpenTelemetryConfigured; + builder.isCustomMeterProviderConfigured() || builder.getMeterProvider() != null; + this.customOpenTelemetryConfigured = builder.isCustomOpenTelemetryConfigured(); this.metricInterval = - builder.metricInterval != null ? builder.metricInterval : Duration.ofSeconds(60); + builder.getMetricInterval() != null ? builder.getMetricInterval() : Duration.ofSeconds(60); boolean hasCustomOtelOrMeterProvider = this.customMeterProviderConfigured || this.customOpenTelemetryConfigured; if (!hasCustomOtelOrMeterProvider && this.metricInterval != null) { @@ -257,6 +287,22 @@ public StorageOptions.Builder setMetricInterval(Duration metricInterval) { } } + @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) { @@ -302,7 +348,7 @@ public static String version() { /** * Whether OpenTelemetry client metrics are enabled. * - * @since 2.74.0 This new api is in preview and is subject to breaking changes. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public boolean isEnableOtelMetrics() { @@ -312,7 +358,7 @@ public boolean isEnableOtelMetrics() { /** * Whether OpenTelemetry debug client metrics are enabled. * - * @since 2.74.0 This new api is in preview and is subject to breaking changes. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public boolean isEnableOtelDebugMetrics() { @@ -322,7 +368,7 @@ public boolean isEnableOtelDebugMetrics() { /** * The {@link MeterProvider} configured for recording client metrics. * - * @since 2.74.0 This new api is in preview and is subject to breaking changes. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public @Nullable MeterProvider getMeterProvider() { @@ -332,7 +378,7 @@ public boolean isEnableOtelDebugMetrics() { /** * 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. + * @since 2.76.0 This new api is in preview and is subject to breaking changes. */ @BetaApi public Duration getMetricInterval() { 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 index 46e1f5494b32..3a44ef23dc10 100644 --- 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 @@ -121,4 +121,64 @@ public void setEnvResolverForTesting_resolution() { 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 index e6df708367c9..366c2d17cfdc 100644 --- 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 @@ -178,6 +178,77 @@ public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() { assertThat(grpcDisabled.isEnableOtelDebugMetrics()).isFalse(); } + @Test + public void debugMetricsGatedOnBaseMetrics_builder() { + HttpStorageOptions httpDebugOnly = + HttpStorageOptions.http() + .setEnableOtelMetrics(false) + .setEnableOtelDebugMetrics(true) + .build(); + assertThat(httpDebugOnly.isEnableOtelMetrics()).isFalse(); + assertThat(httpDebugOnly.isEnableOtelDebugMetrics()).isFalse(); + + GrpcStorageOptions grpcDebugOnly = + GrpcStorageOptions.grpc() + .setEnableOtelMetrics(false) + .setEnableOtelDebugMetrics(true) + .build(); + assertThat(grpcDebugOnly.isEnableOtelMetrics()).isFalse(); + assertThat(grpcDebugOnly.isEnableOtelDebugMetrics()).isFalse(); + + HttpStorageOptions httpBoth = + HttpStorageOptions.http() + .setEnableOtelMetrics(true) + .setEnableOtelDebugMetrics(true) + .build(); + assertThat(httpBoth.isEnableOtelMetrics()).isTrue(); + assertThat(httpBoth.isEnableOtelDebugMetrics()).isTrue(); + + GrpcStorageOptions grpcBoth = + GrpcStorageOptions.grpc() + .setEnableOtelMetrics(true) + .setEnableOtelDebugMetrics(true) + .build(); + assertThat(grpcBoth.isEnableOtelMetrics()).isTrue(); + assertThat(grpcBoth.isEnableOtelDebugMetrics()).isTrue(); + } + + @Test + public void debugMetricsGatedOnBaseMetrics_env() { + 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 + 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 explicitly false in env -> debug metrics should evaluate to false + env.put(StorageMetricsConfig.ENV_ENABLE_OTEL_METRICS_JAVA, "false"); + HttpStorageOptions httpEnvExplicitFalse = HttpStorageOptions.http().build(); + assertThat(httpEnvExplicitFalse.isEnableOtelMetrics()).isFalse(); + assertThat(httpEnvExplicitFalse.isEnableOtelDebugMetrics()).isFalse(); + + GrpcStorageOptions grpcEnvExplicitFalse = GrpcStorageOptions.grpc().build(); + assertThat(grpcEnvExplicitFalse.isEnableOtelMetrics()).isFalse(); + assertThat(grpcEnvExplicitFalse.isEnableOtelDebugMetrics()).isFalse(); + + // Base metrics enabled in env -> debug metrics should evaluate 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(); + } + @Test public void permissiveBooleanParsing_trueValues() { String[] trueValues = {"1", "t", "T", "true", "TRUE", "True", " true "}; From e7a358831a38b839cf3bf489ca46654eaa77786b Mon Sep 17 00:00:00 2001 From: Nidhi Nandwani Date: Tue, 20 Oct 2026 09:03:03 +0000 Subject: [PATCH 28/28] test(storage): deduplicate HTTP and gRPC test cases in StorageOptionsTest [Generated-by: AI] --- .../cloud/storage/StorageOptionsTest.java | 648 +++++++----------- 1 file changed, 249 insertions(+), 399 deletions(-) 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 index 366c2d17cfdc..f2dc6f3fda8d 100644 --- 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 @@ -35,218 +35,159 @@ 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() { - 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)); + 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() { + public void builder_explicitEnabling() throws Exception { 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); + 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() { + 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); - 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(); + 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"); - 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(); + forEachTransport( + builder -> { + StorageOptions enabled = + builder.setEnableOtelMetrics(true).setEnableOtelDebugMetrics(true).build(); + assertThat(enabled.isEnableOtelMetrics()).isTrue(); + assertThat(enabled.isEnableOtelDebugMetrics()).isTrue(); + }); } @Test - public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() { + public void precedence_builderTakesPrecedenceOverDefaultWhenEnvUnset() throws Exception { 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(); + 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() { - HttpStorageOptions httpDebugOnly = - HttpStorageOptions.http() - .setEnableOtelMetrics(false) - .setEnableOtelDebugMetrics(true) - .build(); - assertThat(httpDebugOnly.isEnableOtelMetrics()).isFalse(); - assertThat(httpDebugOnly.isEnableOtelDebugMetrics()).isFalse(); - - GrpcStorageOptions grpcDebugOnly = - GrpcStorageOptions.grpc() - .setEnableOtelMetrics(false) - .setEnableOtelDebugMetrics(true) - .build(); - assertThat(grpcDebugOnly.isEnableOtelMetrics()).isFalse(); - assertThat(grpcDebugOnly.isEnableOtelDebugMetrics()).isFalse(); - - HttpStorageOptions httpBoth = - HttpStorageOptions.http() - .setEnableOtelMetrics(true) - .setEnableOtelDebugMetrics(true) - .build(); - assertThat(httpBoth.isEnableOtelMetrics()).isTrue(); - assertThat(httpBoth.isEnableOtelDebugMetrics()).isTrue(); - - GrpcStorageOptions grpcBoth = - GrpcStorageOptions.grpc() - .setEnableOtelMetrics(true) - .setEnableOtelDebugMetrics(true) - .build(); - assertThat(grpcBoth.isEnableOtelMetrics()).isTrue(); - assertThat(grpcBoth.isEnableOtelDebugMetrics()).isTrue(); + 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() { + 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 - 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(); + 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"); - HttpStorageOptions httpEnvExplicitFalse = HttpStorageOptions.http().build(); - assertThat(httpEnvExplicitFalse.isEnableOtelMetrics()).isFalse(); - assertThat(httpEnvExplicitFalse.isEnableOtelDebugMetrics()).isFalse(); - - GrpcStorageOptions grpcEnvExplicitFalse = GrpcStorageOptions.grpc().build(); - assertThat(grpcEnvExplicitFalse.isEnableOtelMetrics()).isFalse(); - assertThat(grpcEnvExplicitFalse.isEnableOtelDebugMetrics()).isFalse(); + 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"); - 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(); + forEachTransport( + builder -> { + StorageOptions envBoth = builder.build(); + assertThat(envBoth.isEnableOtelMetrics()).isTrue(); + assertThat(envBoth.isEnableOtelDebugMetrics()).isTrue(); + }); } @Test @@ -326,286 +267,195 @@ public void permissiveBooleanParsing_nullHandled() { } @Test - public void toBuilder_doesNotBakeInEnvironmentVariable() { + public void toBuilder_doesNotBakeInEnvironmentVariable() 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); - - 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(); + 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() { + 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); - 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(); + forEachTransport( + builder -> { + StorageOptions options = builder.build(); + assertThat(options.isEnableOtelMetrics()).isTrue(); + assertThat(options.isEnableOtelDebugMetrics()).isTrue(); + }); } @Test - public void setMetricInterval_rejectsNullZeroOrNegative() { - 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)); + 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() { + public void setMetricInterval_rejectsSubSixtySecondsWithoutCustomProvider() throws Exception { // 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()); + 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() { - 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)); + 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() { + public void setMetricInterval_acceptsSubSixtySecondsWithCustomMeterProvider() throws Exception { MeterProvider customMeterProvider = SdkMeterProvider.builder().build(); - 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)); + forEachTransport( + builder -> { + StorageOptions customOptions = + builder + .setMeterProvider(customMeterProvider) + .setMetricInterval(Duration.ofSeconds(30)) + .build(); + assertThat(customOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); + }); } @Test - public void setMetricInterval_acceptsSubSixtySecondsWithCustomOpenTelemetry() { + public void setMetricInterval_acceptsSubSixtySecondsWithCustomOpenTelemetry() throws Exception { 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)); + 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(); - 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(); + 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(); - HttpStorageOptions httpOptions = - HttpStorageOptions.http() - .setMeterProvider(customMeterProvider) - .setMetricInterval(Duration.ofSeconds(30)) - .build(); - assertThat(httpOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); - - 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.getMeterProvider()).isNull(); - assertThat(deserializedHttp.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); - - GrpcStorageOptions grpcOptions = - GrpcStorageOptions.grpc() - .setMeterProvider(customMeterProvider) - .setMetricInterval(Duration.ofSeconds(30)) - .build(); - assertThat(grpcOptions.getMetricInterval()).isEqualTo(Duration.ofSeconds(30)); - - 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.getMeterProvider()).isNull(); - assertThat(deserializedGrpc.getMetricInterval()).isEqualTo(Duration.ofSeconds(60)); + 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 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()); + 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()); + }); } }