Repository: googleapis/java-storage
Pull request #2653 (2024-07-31 02:31:27)

chore: modify readme's transport support description
Solves https://github.com/googleapis/java-storage/pull/2631#discussion_r1681551588


Patch:
diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml
index 6b6b9030..d0f61d51 100644
--- a/.github/sync-repo-settings.yaml
+++ b/.github/sync-repo-settings.yaml
@@ -18,7 +18,8 @@ branchProtectionRules:
       - 'Kokoro - Test: Java GraalVM Native Image'
       - 'Kokoro - Test: Java 17 GraalVM Native Image'
       - javadoc
-      - unmanaged_dependency_check
+#      TODO: re-enabled once https://github.com/googleapis/sdk-platform-java/pull/3078 is merged and released
+#      - unmanaged_dependency_check
   - pattern: 1.113.14-sp
     isAdminEnforced: true
     requiredApprovingReviewCount: 1
diff --git a/.readme-partials.yaml b/.readme-partials.yaml
index adc8597c..0cb64e02 100644
--- a/.readme-partials.yaml
+++ b/.readme-partials.yaml
@@ -32,19 +32,19 @@ custom_content: |
   <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage-control</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
   </dependency>
   ```
   
   If you are using Gradle 5.x or later, add this to your dependencies:
   ```Groovy
-  implementation platform('com.google.cloud:libraries-bom:2.40.1')
+  implementation platform('com.google.cloud:libraries-bom:2.41.0')
   implementation 'com.google.cloud:google-cloud-storage-control'
   ```
   If you are using Gradle without BOM, add this to your dependencies:
   
   ```Groovy
-  implementation 'com.google.cloud:google-cloud-storage-control:2.40.2-SNAPSHOT' <!-- {x-version-update:google-cloud-storage-control:current} -->
+  implementation 'com.google.cloud:google-cloud-storage-control:2.41.0' <!-- {x-version-update:google-cloud-storage-control:current} -->
   ```
   
   #### Creating an authorized service object
diff --git a/.repo-metadata.json b/.repo-metadata.json
index bc02423b..18fe4b0c 100644
--- a/.repo-metadata.json
+++ b/.repo-metadata.json
@@ -5,7 +5,7 @@
   "api_description": "is a durable and highly available object storage service. Google Cloud Storage is almost infinitely scalable and guarantees consistency: when a write succeeds, the latest copy of the object will be returned to any GET, globally.",
   "client_documentation": "https://cloud.google.com/java/docs/reference/google-cloud-storage/latest/history",
   "release_level": "stable",
-  "transport": "grpc",
+  "transport": "http",
   "language": "java",
   "repo": "googleapis/java-storage",
   "repo_short": "java-storage",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5821bc84..a889954b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
 # Changelog
 
+## [2.41.0](https://github.com/googleapis/java-storage/compare/v2.40.1...v2.41.0) (2024-07-31)
+
+
+### Features
+
+* Enable gRPC client open telemetry metrics reporting ([#2590](https://github.com/googleapis/java-storage/issues/2590)) ([d153228](https://github.com/googleapis/java-storage/commit/d153228a301007b5952de9722f370dda0784473a))
+
+
+### Bug Fixes
+
+* Add UnknownHostException to set of retriable exception ([#2651](https://github.com/googleapis/java-storage/issues/2651)) ([18de9fc](https://github.com/googleapis/java-storage/commit/18de9fcdb831132336eca4112dfe0515174bba7b))
+* Update grpc resumable upload error categorization to be more tolerant ([#2644](https://github.com/googleapis/java-storage/issues/2644)) ([95697dd](https://github.com/googleapis/java-storage/commit/95697dd3d744351058c13793c6ae576820f6b638))
+* Update Storage#readAllBytes to respect shouldReturnRawInputStream option ([#2635](https://github.com/googleapis/java-storage/issues/2635)) ([dc883cc](https://github.com/googleapis/java-storage/commit/dc883cce5f547def7cfb34c4f8a2d409493e4cb9))
+* Update TransferManager downloads to reduce in memory buffering ([#2630](https://github.com/googleapis/java-storage/issues/2630)) ([fc2fd75](https://github.com/googleapis/java-storage/commit/fc2fd750ed60b840e6285a4b1f4ecce739df4c09))
+* Use fast calculation for totalRemaining number of bytes from multiple ByteBuffers ([#2633](https://github.com/googleapis/java-storage/issues/2633)) ([758b3dd](https://github.com/googleapis/java-storage/commit/758b3dd3cc4f6dfc2dfc12c3a77472d97c31c5d5))
+
+
+### Dependencies
+
+* Update dependency com.google.apis:google-api-services-storage to v1-rev20240625-2.0.0 ([#2616](https://github.com/googleapis/java-storage/issues/2616)) ([b22babb](https://github.com/googleapis/java-storage/commit/b22babbe26572d8c4289a65a0b125b2a60e8ef79))
+* Update dependency com.google.apis:google-api-services-storage to v1-rev20240706-2.0.0 ([#2634](https://github.com/googleapis/java-storage/issues/2634)) ([1ccaa0c](https://github.com/googleapis/java-storage/commit/1ccaa0c64887a0661438957e9427237ee005ccf1))
+* Update dependency com.google.cloud:sdk-platform-java-config to v3.33.0 ([#2647](https://github.com/googleapis/java-storage/issues/2647)) ([8196259](https://github.com/googleapis/java-storage/commit/8196259927330ecfe3e604c24d248f7935e7fe0d))
+* Update dependency net.jqwik:jqwik to v1.9.0 ([#2608](https://github.com/googleapis/java-storage/issues/2608)) ([a20eb66](https://github.com/googleapis/java-storage/commit/a20eb660ddfa4b68d79ce04496064f3025676d5a))
+* Update dependency org.junit.vintage:junit-vintage-engine to v5.10.3 ([#2604](https://github.com/googleapis/java-storage/issues/2604)) ([8c79f39](https://github.com/googleapis/java-storage/commit/8c79f39ad78d100065c189bcf8e18644b29ff9ed))
+* Update junit-platform.version to v5.10.3 ([#2605](https://github.com/googleapis/java-storage/issues/2605)) ([a532ee4](https://github.com/googleapis/java-storage/commit/a532ee49e2ff5972ea8a2aabbab2dcf6fe0df774))
+
 ## [2.40.1](https://github.com/googleapis/java-storage/compare/v2.40.0...v2.40.1) (2024-06-26)
 
 
diff --git a/README.md b/README.md
index dada3cb6..5dd0bacd 100644
--- a/README.md
+++ b/README.md
@@ -46,12 +46,12 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.40.1</version>
+  <version>2.41.0</version>
 </dependency>
 <dependency>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-control</artifactId>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
 </dependency>
 
 ```
@@ -59,20 +59,20 @@ If you are using Maven without the BOM, add this to your dependencies:
 If you are using Gradle 5.x or later, add this to your dependencies:
 
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:26.43.0')
+implementation platform('com.google.cloud:libraries-bom:2.41.0')
 
 implementation 'com.google.cloud:google-cloud-storage'
 ```
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage:2.40.1'
+implementation 'com.google.cloud:google-cloud-storage:2.41.0'
 ```
 
 If you are using SBT, add this to your dependencies:
 
 ```Scala
-libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.40.1"
+libraryDependencies += "com.google.cloud" % "google-cloud-storage" % "2.41.0"
 ```
 <!-- {x-version-update-end} -->
 
@@ -141,19 +141,19 @@ If you are using Maven without the BOM, add this to your dependencies:
 <dependency>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-control</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
 </dependency>
 ```
 
 If you are using Gradle 5.x or later, add this to your dependencies:
 ```Groovy
-implementation platform('com.google.cloud:libraries-bom:2.40.1')
+implementation platform('com.google.cloud:libraries-bom:2.41.0')
 implementation 'com.google.cloud:google-cloud-storage-control'
 ```
 If you are using Gradle without BOM, add this to your dependencies:
 
 ```Groovy
-implementation 'com.google.cloud:google-cloud-storage-control:2.40.2-SNAPSHOT' <!-- {x-version-update:google-cloud-storage-control:current} -->
+implementation 'com.google.cloud:google-cloud-storage-control:2.41.0' <!-- {x-version-update:google-cloud-storage-control:current} -->
 ```
 
 #### Creating an authorized service object
@@ -414,7 +414,7 @@ To get help, follow the instructions in the [shared Troubleshooting document][tr
 
 ## Transport
 
-Cloud Storage uses gRPC for the transport layer.
+Cloud Storage uses HTTP/JSON for the transport layer.
 
 ## Supported Java Versions
 
@@ -508,7 +508,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
 [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-storage/java11.html
 [stability-image]: https://img.shields.io/badge/stability-stable-green
 [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-storage.svg
-[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.40.1
+[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-storage/2.41.0
 [authentication]: https://github.com/googleapis/google-cloud-java#authentication
 [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
 [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles
diff --git a/gapic-google-cloud-storage-v2/pom.xml b/gapic-google-cloud-storage-v2/pom.xml
index 76bc3173..7a22e710 100644
--- a/gapic-google-cloud-storage-v2/pom.xml
+++ b/gapic-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>gapic-google-cloud-storage-v2</artifactId>
-  <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+  <version>2.41.0-alpha</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
   <name>gapic-google-cloud-storage-v2</name>
   <description>GRPC library for gapic-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/generation_config.yaml b/generation_config.yaml
index 3ffd6c0a..5e8d73b6 100644
--- a/generation_config.yaml
+++ b/generation_config.yaml
@@ -1,5 +1,5 @@
 gapic_generator_version: 2.43.0
-googleapis_commitish: 0fa9ce880be5ea7c3027015849cd4fbfb04812c5
+googleapis_commitish: d8fce50eea92bac3a6612ee61559989ce3b38776
 libraries_bom_version: 26.43.0
 libraries:
   - api_shortname: storage
@@ -20,6 +20,7 @@ libraries:
     extra_versioned_modules: gapic-google-cloud-storage-v2
     excluded_poms: google-cloud-storage-bom,google-cloud-storage
     recommended_package: com.google.cloud.storage
+    transport: rest
     GAPICs:
       - proto_path: google/storage/v2
       - proto_path: google/storage/control/v2
diff --git a/google-cloud-storage-bom/pom.xml b/google-cloud-storage-bom/pom.xml
index a91a0dd3..28c2f85e 100644
--- a/google-cloud-storage-bom/pom.xml
+++ b/google-cloud-storage-bom/pom.xml
@@ -19,7 +19,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-bom</artifactId>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>pom</packaging>
   <parent>
     <groupId>com.google.cloud</groupId>
@@ -69,37 +69,37 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:gapic-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-control</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+        <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+        <version>2.41.0</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+        <version>2.41.0</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
       </dependency>
     </dependencies>
   </dependencyManagement>
diff --git a/google-cloud-storage-control/pom.xml b/google-cloud-storage-control/pom.xml
index 799e37f0..fe1aa8ed 100644
--- a/google-cloud-storage-control/pom.xml
+++ b/google-cloud-storage-control/pom.xml
@@ -5,13 +5,13 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-control</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
     <name>google-cloud-storage-control</name>
     <description>GRPC library for google-cloud-storage-control</description>
     <parent>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-parent</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
     </parent>
 
     <dependencies>
diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index b6bc155e..fbc9e99a 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -2,7 +2,7 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>google-cloud-storage</artifactId>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   <packaging>jar</packaging>
   <name>Google Cloud Storage</name>
   <url>https://github.com/googleapis/java-storage</url>
@@ -12,7 +12,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <properties>
     <site.installationModule>google-cloud-storage</site.installationModule>
@@ -124,6 +124,53 @@
       <groupId>com.google.api.grpc</groupId>
       <artifactId>gapic-google-cloud-storage-v2</artifactId>
     </dependency>
+    <!-- Open Telemetry dependencies -->
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.grpc</groupId>
+      <artifactId>grpc-opentelemetry</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-api</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-metrics</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-common</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>io.opentelemetry</groupId>
+      <artifactId>opentelemetry-sdk-extension-autoconfigure-spi</artifactId>
+    </dependency>
+
+    <dependency>
+    <groupId>io.opentelemetry.semconv</groupId>
+    <artifactId>opentelemetry-semconv</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.cloud.opentelemetry</groupId>
+      <artifactId>exporter-metrics</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>io.opentelemetry.contrib</groupId>
+      <artifactId>opentelemetry-gcp-resources</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.checkerframework</groupId>
+      <artifactId>checker-qual</artifactId>
+    </dependency>
 
     <!-- Access to exception for retry handling -->
     <dependency>
@@ -159,10 +206,7 @@
       <artifactId>grpc-googleapis</artifactId>
       <scope>runtime</scope>
     </dependency>
-    <dependency>
-      <groupId>org.checkerframework</groupId>
-      <artifactId>checker-qual</artifactId>
-    </dependency>
+
     <!--
     We're not using this directly, however there appears to be some resolution
     issues when we don't list this dependency. Specifically, the check to ensure the flattened
@@ -184,6 +228,7 @@
       <version>0.140.0</version>
       <scope>test</scope>
     </dependency>
+
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-kms</artifactId>
@@ -307,6 +352,12 @@
               Add in vintage engine so that both JUnit4 and JUnit5 tests are run by the JUnit5 runner.
               -->
               <dependency>org.junit.vintage:junit-vintage-engine</dependency>
+
+              <!--
+              These are needed at runtime, but it causes unused declared dependency errors
+              -->
+              <dependency>io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi</dependency>
+              <dependency>io.opentelemetry.semconv:opentelemetry-semconv</dependency>
             </ignoredDependencies>
           </configuration>
         </plugin>
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java
index cf85b740..b9a8df0b 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/DefaultStorageRetryStrategy.java
@@ -26,6 +26,7 @@ import com.google.common.collect.ImmutableSet;
 import com.google.gson.stream.MalformedJsonException;
 import java.io.IOException;
 import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.util.Set;
 import javax.net.ssl.SSLException;
 
@@ -114,6 +115,8 @@ final class DefaultStorageRetryStrategy implements StorageRetryStrategy {
           SocketException se = (SocketException) cause;
           return shouldRetryIOException(se);
         }
+      } else if (ioException instanceof UnknownHostException && idempotent) {
+        return RetryResult.RETRY;
       }
       if (BaseServiceException.isRetryable(idempotent, ioException)) {
         return RetryResult.RETRY;
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
index 92081e03..64376cc1 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java
@@ -116,6 +116,9 @@ public final class GrpcStorageOptions extends StorageOptions
   private final GrpcRetryAlgorithmManager retryAlgorithmManager;
   private final Duration terminationAwaitDuration;
   private final boolean attemptDirectPath;
+  private final boolean enableGrpcClientMetrics;
+
+  private final boolean grpcClientMetricsManuallyEnabled;
   private final GrpcInterceptorProvider grpcInterceptorProvider;
   private final BlobWriteSessionConfig blobWriteSessionConfig;
 
@@ -129,6 +132,8 @@ public final class GrpcStorageOptions extends StorageOptions
         MoreObjects.firstNonNull(
             builder.terminationAwaitDuration, serviceDefaults.getTerminationAwaitDuration());
     this.attemptDirectPath = builder.attemptDirectPath;
+    this.enableGrpcClientMetrics = builder.enableGrpcClientMetrics;
+    this.grpcClientMetricsManuallyEnabled = builder.grpcMetricsManuallyEnabled;
     this.grpcInterceptorProvider = builder.grpcInterceptorProvider;
     this.blobWriteSessionConfig = builder.blobWriteSessionConfig;
   }
@@ -287,6 +292,16 @@ public final class GrpcStorageOptions extends StorageOptions
     if (scheme.equals("http")) {
       channelProviderBuilder.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
     }
+
+    if (enableGrpcClientMetrics) {
+      OpenTelemetryBootstrappingUtils.enableGrpcMetrics(
+          channelProviderBuilder,
+          endpoint,
+          this.getProjectId(),
+          this.getUniverseDomain(),
+          !grpcClientMetricsManuallyEnabled);
+    }
+
     builder.setTransportChannelProvider(channelProviderBuilder.build());
     RetrySettings baseRetrySettings = getRetrySettings();
     RetrySettings readRetrySettings =
@@ -350,6 +365,7 @@ public final class GrpcStorageOptions extends StorageOptions
         retryAlgorithmManager,
         terminationAwaitDuration,
         attemptDirectPath,
+        enableGrpcClientMetrics,
         grpcInterceptorProvider,
         blobWriteSessionConfig,
         baseHashCode());
@@ -365,6 +381,7 @@ public final class GrpcStorageOptions extends StorageOptions
     }
     GrpcStorageOptions that = (GrpcStorageOptions) o;
     return attemptDirectPath == that.attemptDirectPath
+        && enableGrpcClientMetrics == that.enableGrpcClientMetrics
         && Objects.equals(retryAlgorithmManager, that.retryAlgorithmManager)
         && Objects.equals(terminationAwaitDuration, that.terminationAwaitDuration)
         && Objects.equals(grpcInterceptorProvider, that.grpcInterceptorProvider)
@@ -408,11 +425,15 @@ public final class GrpcStorageOptions extends StorageOptions
     private StorageRetryStrategy storageRetryStrategy;
     private Duration terminationAwaitDuration;
     private boolean attemptDirectPath = GrpcStorageDefaults.INSTANCE.isAttemptDirectPath();
+    private boolean enableGrpcClientMetrics =
+        GrpcStorageDefaults.INSTANCE.isEnableGrpcClientMetrics();
     private GrpcInterceptorProvider grpcInterceptorProvider =
         GrpcStorageDefaults.INSTANCE.grpcInterceptorProvider();
     private BlobWriteSessionConfig blobWriteSessionConfig =
         GrpcStorageDefaults.INSTANCE.getDefaultStorageWriterConfig();
 
+    private boolean grpcMetricsManuallyEnabled = false;
+
     Builder() {}
 
     Builder(StorageOptions options) {
@@ -421,6 +442,7 @@ public final class GrpcStorageOptions extends StorageOptions
       this.storageRetryStrategy = gso.getRetryAlgorithmManager().retryStrategy;
       this.terminationAwaitDuration = gso.getTerminationAwaitDuration();
       this.attemptDirectPath = gso.attemptDirectPath;
+      this.enableGrpcClientMetrics = gso.enableGrpcClientMetrics;
       this.grpcInterceptorProvider = gso.grpcInterceptorProvider;
       this.blobWriteSessionConfig = gso.blobWriteSessionConfig;
     }
@@ -454,6 +476,21 @@ public final class GrpcStorageOptions extends StorageOptions
       this.attemptDirectPath = attemptDirectPath;
       return this;
     }
+    /**
+     * Option for whether this client should emit internal gRPC client internal metrics to Cloud
+     * Monitoring. To disable metric reporting, set this to false. True by default. Emitting metrics
+     * is free and requires minimal CPU and memory.
+     *
+     * @since 2.41.0 This new api is in preview and is subject to breaking changes.
+     */
+    @BetaApi
+    public GrpcStorageOptions.Builder setEnableGrpcClientMetrics(boolean enableGrpcClientMetrics) {
+      this.enableGrpcClientMetrics = enableGrpcClientMetrics;
+      if (enableGrpcClientMetrics) {
+        grpcMetricsManuallyEnabled = true;
+      }
+      return this;
+    }
 
     /** @since 2.14.0 This new api is in preview and is subject to breaking changes. */
     @BetaApi
@@ -660,6 +697,12 @@ public final class GrpcStorageOptions extends StorageOptions
       return false;
     }
 
+    /** @since 2.41.0 This new api is in preview and is subject to breaking changes. */
+    @BetaApi
+    public boolean isEnableGrpcClientMetrics() {
+      return true;
+    }
+
     /** @since 2.22.3 This new api is in preview and is subject to breaking changes. */
     @BetaApi
     public GrpcInterceptorProvider grpcInterceptorProvider() {
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java
new file mode 100644
index 00000000..ed0ba544
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/OpenTelemetryBootstrappingUtils.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import com.google.api.core.ApiFunction;
+import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
+import com.google.api.gax.rpc.PermissionDeniedException;
+import com.google.api.gax.rpc.UnavailableException;
+import com.google.cloud.opentelemetry.metric.GoogleCloudMetricExporter;
+import com.google.cloud.opentelemetry.metric.MetricConfiguration;
+import com.google.cloud.opentelemetry.metric.MonitoredResourceDescription;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.opentelemetry.GrpcOpenTelemetry;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.internal.StringUtils;
+import io.opentelemetry.contrib.gcp.resource.GCPResourceProvider;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.common.export.MemoryMode;
+import io.opentelemetry.sdk.metrics.Aggregation;
+import io.opentelemetry.sdk.metrics.InstrumentSelector;
+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.data.AggregationTemporality;
+import io.opentelemetry.sdk.metrics.data.MetricData;
+import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
+import io.opentelemetry.sdk.metrics.export.MetricExporter;
+import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader;
+import io.opentelemetry.sdk.resources.Resource;
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.net.NoRouteToHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+final class OpenTelemetryBootstrappingUtils {
+  private static final Collection<String> METRICS_TO_ENABLE =
+      ImmutableList.of(
+          "grpc.lb.wrr.rr_fallback",
+          "grpc.lb.wrr.endpoint_weight_not_yet_usable",
+          "grpc.lb.wrr.endpoint_weight_stale",
+          "grpc.lb.wrr.endpoint_weights",
+          "grpc.lb.rls.cache_entries",
+          "grpc.lb.rls.cache_size",
+          "grpc.lb.rls.default_target_picks",
+          "grpc.lb.rls.target_picks",
+          "grpc.lb.rls.failed_picks",
+          "grpc.xds_client.connected",
+          "grpc.xds_client.server_failure",
+          "grpc.xds_client.resource_updates_valid",
+          "grpc.xds_client.resource_updates_invalid",
+          "grpc.xds_client.resources");
+
+  private static final Collection<String> METRICS_ENABLED_BY_DEFAULT =
+      ImmutableList.of(
+          "grpc.client.attempt.sent_total_compressed_message_size",
+          "grpc.client.attempt.rcvd_total_compressed_message_size",
+          "grpc.client.attempt.started",
+          "grpc.client.attempt.duration",
+          "grpc.client.call.duration");
+
+  static final Logger log = Logger.getLogger(OpenTelemetryBootstrappingUtils.class.getName());
+
+  static void enableGrpcMetrics(
+      InstantiatingGrpcChannelProvider.Builder channelProviderBuilder,
+      String endpoint,
+      String projectId,
+      String universeDomain,
+      boolean shouldSuppressExceptions) {
+    String metricServiceEndpoint = getCloudMonitoringEndpoint(endpoint, universeDomain);
+    SdkMeterProvider provider =
+        createMeterProvider(metricServiceEndpoint, projectId, shouldSuppressExceptions);
+
+    OpenTelemetrySdk openTelemetrySdk =
+        OpenTelemetrySdk.builder().setMeterProvider(provider).build();
+    GrpcOpenTelemetry grpcOpenTelemetry =
+        GrpcOpenTelemetry.newBuilder()
+            .sdk(openTelemetrySdk)
+            .enableMetrics(METRICS_TO_ENABLE)
+            .build();
+    ApiFunction<ManagedChannelBuilder, ManagedChannelBuilder> channelConfigurator =
+        channelProviderBuilder.getChannelConfigurator();
+    channelProviderBuilder.setChannelConfigurator(
+        b -> {
+          grpcOpenTelemetry.configureChannelBuilder(b);
+          if (channelConfigurator != null) {
+            return channelConfigurator.apply(b);
+          }
+          return b;
+        });
+  }
+
+  @VisibleForTesting
+  static String getCloudMonitoringEndpoint(String endpoint, String universeDomain) {
+    String metricServiceEndpoint = "monitoring.googleapis.com";
+
+    // use contains instead of equals because endpoint has a port in it
+    if (universeDomain != null && endpoint.contains("storage." + universeDomain)) {
+      metricServiceEndpoint = "monitoring." + universeDomain;
+    } else if (!endpoint.contains("storage.googleapis.com")) {
+      String canonicalEndpoint = "storage.googleapis.com";
+      String privateEndpoint = "private.googleapis.com";
+      String restrictedEndpoint = "restricted.googleapis.com";
+      if (universeDomain != null) {
+        canonicalEndpoint = "storage." + universeDomain;
+        privateEndpoint = "private." + universeDomain;
+        restrictedEndpoint = "restricted." + universeDomain;
+      }
+      String match =
+          ImmutableList.of(canonicalEndpoint, privateEndpoint, restrictedEndpoint).stream()
+              .filter(s -> endpoint.contains(s) || endpoint.contains("google-c2p:///" + s))
+              .collect(Collectors.joining());
+      if (!StringUtils.isNullOrEmpty(match)) {
+        metricServiceEndpoint = match;
+      }
+    }
+    return metricServiceEndpoint + ":" + endpoint.split(":")[1];
+  }
+
+  @VisibleForTesting
+  static SdkMeterProvider createMeterProvider(
+      String metricServiceEndpoint, String projectId, boolean shouldSuppressExceptions) {
+    GCPResourceProvider resourceProvider = new GCPResourceProvider();
+    Attributes detectedAttributes = resourceProvider.getAttributes();
+
+    String detectedProjectId = detectedAttributes.get(AttributeKey.stringKey("cloud.account.id"));
+    String projectIdToUse = detectedProjectId == null ? projectId : detectedProjectId;
+
+    if (!projectIdToUse.equals(projectId)) {
+      log.warning(
+          "The Project ID configured for metrics is "
+              + projectIdToUse
+              + ", but the Project ID of the storage client is "
+              + projectId
+              + ". Make sure that the service account in use has the required metric writing role "
+              + "(roles/monitoring.metricWriter) in the project "
+              + projectIdToUse
+              + ", or metrics will not be written.");
+    }
+
+    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());
+    }
+    MetricExporter exporter =
+        shouldSuppressExceptions
+            ? new PermissionDeniedSingleReportMetricsExporter(cloudMonitoringExporter)
+            : cloudMonitoringExporter;
+    providerBuilder
+        .registerMetricReader(
+            PeriodicMetricReader.builder(exporter)
+                .setInterval(java.time.Duration.ofSeconds(60))
+                .build())
+        .setResource(
+            Resource.create(
+                Attributes.builder()
+                    .put("gcp.resource_type", "storage.googleapis.com/Client")
+                    .put("location", detectedAttributes.get(AttributeKey.stringKey("cloud.region")))
+                    .put("project_id", projectIdToUse)
+                    .put(
+                        "cloud_platform",
+                        detectedAttributes.get(AttributeKey.stringKey("cloud.platform")))
+                    .put("host_id", detectedAttributes.get(AttributeKey.stringKey("host.id")))
+                    .put("instance_id", UUID.randomUUID().toString())
+                    .put("api", "grpc")
+                    .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();
+  }
+
+  private static void addHistogramView(
+      SdkMeterProviderBuilder provider, List<Double> boundaries, String name, String unit) {
+    InstrumentSelector instrumentSelector =
+        InstrumentSelector.builder()
+            .setType(InstrumentType.HISTOGRAM)
+            .setUnit(unit)
+            .setName(name)
+            .setMeterName("grpc-java")
+            .setMeterSchemaUrl("")
+            .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 List<Double> latencyHistogramBoundaries() {
+    List<Double> boundaries = new ArrayList<>();
+    BigDecimal boundary = new BigDecimal(0, MathContext.UNLIMITED);
+    BigDecimal increment = new BigDecimal("0.002", MathContext.UNLIMITED); // 2ms
+
+    // 2ms buckets for the first 100ms, so we can have higher resolution for uploads and downloads
+    // in the 100 KiB range
+    for (int i = 0; i != 50; i++) {
+      boundaries.add(boundary.doubleValue());
+      boundary = boundary.add(increment);
+    }
+
+    // For the remaining buckets do 10 10ms, 10 20ms, and so on, up until 5 minutes
+    increment = new BigDecimal("0.01", MathContext.UNLIMITED); // 10 ms
+    for (int i = 0; i != 150 && boundary.compareTo(new BigDecimal(300)) < 1; i++) {
+      boundaries.add(boundary.doubleValue());
+      if (i != 0 && i % 10 == 0) {
+        increment = increment.multiply(new BigDecimal(2));
+      }
+      boundary = boundary.add(increment);
+    }
+
+    return boundaries;
+  }
+
+  private static List<Double> sizeHistogramBoundaries() {
+    long kb = 1024;
+    long mb = 1024 * kb;
+    long gb = 1024 * mb;
+
+    List<Double> boundaries = new ArrayList<>();
+    long boundary = 0;
+    long increment = 128 * kb;
+
+    // 128 KiB increments up to 4MiB, then exponential growth
+    while (boundaries.size() < 200 && boundary <= 16 * gb) {
+      boundaries.add((double) boundary);
+      boundary += increment;
+      if (boundary >= 4 * mb) {
+        increment *= 2;
+      }
+    }
+    return boundaries;
+  }
+
+  private static final class PermissionDeniedSingleReportMetricsExporter implements MetricExporter {
+    private final MetricExporter delegate;
+    private final AtomicBoolean seenPermissionDenied = new AtomicBoolean(false);
+    private final AtomicBoolean seenNoRouteToHost = new AtomicBoolean(false);
+
+    private PermissionDeniedSingleReportMetricsExporter(MetricExporter delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public CompletableResultCode export(Collection<MetricData> metrics) {
+      if (seenPermissionDenied.get() && seenNoRouteToHost.get()) {
+        return CompletableResultCode.ofFailure();
+      }
+
+      try {
+        return delegate.export(metrics);
+      } catch (PermissionDeniedException e) {
+        if (!seenPermissionDenied.get()) {
+          seenPermissionDenied.set(true);
+          throw e;
+        }
+        return CompletableResultCode.ofFailure();
+      } catch (UnavailableException e) {
+        if (seenPermissionDenied.get()
+            && !seenNoRouteToHost.get()
+            && ultimateCause(e, NoRouteToHostException.class)) {
+          seenNoRouteToHost.set(true);
+          throw e;
+        }
+        return CompletableResultCode.ofFailure();
+      }
+    }
+
+    @Override
+    public Aggregation getDefaultAggregation(InstrumentType instrumentType) {
+      return delegate.getDefaultAggregation(instrumentType);
+    }
+
+    @Override
+    public MemoryMode getMemoryMode() {
+      return delegate.getMemoryMode();
+    }
+
+    @Override
+    public CompletableResultCode flush() {
+      return delegate.flush();
+    }
+
+    @Override
+    public CompletableResultCode shutdown() {
+      return delegate.shutdown();
+    }
+
+    @Override
+    public void close() {
+      delegate.close();
+    }
+
+    @Override
+    public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) {
+      return delegate.getAggregationTemporality(instrumentType);
+    }
+
+    @Override
+    public DefaultAggregationSelector with(InstrumentType instrumentType, Aggregation aggregation) {
+      return delegate.with(instrumentType, aggregation);
+    }
+
+    private static boolean ultimateCause(Throwable t, Class<? extends Throwable> c) {
+      if (t == null) {
+        return false;
+      }
+
+      Throwable cause = t.getCause();
+      if (cause != null && c.isAssignableFrom(cause.getClass())) {
+        return true;
+      } else {
+        return ultimateCause(cause, c);
+      }
+    }
+  }
+}
diff --git a/grpc-google-cloud-storage-control-v2/pom.xml b/grpc-google-cloud-storage-control-v2/pom.xml
index b102687f..cd85deba 100644
--- a/grpc-google-cloud-storage-control-v2/pom.xml
+++ b/grpc-google-cloud-storage-control-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+  <version>2.41.0</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
   <name>grpc-google-cloud-storage-control-v2</name>
   <description>GRPC library for google-cloud-storage</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/grpc-google-cloud-storage-v2/pom.xml b/grpc-google-cloud-storage-v2/pom.xml
index 5256d6ce..2f788f3b 100644
--- a/grpc-google-cloud-storage-v2/pom.xml
+++ b/grpc-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>grpc-google-cloud-storage-v2</artifactId>
-  <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+  <version>2.41.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
   <name>grpc-google-cloud-storage-v2</name>
   <description>GRPC library for grpc-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/pom.xml b/pom.xml
index 6087fd29..eb1f4212 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
   <groupId>com.google.cloud</groupId>
   <artifactId>google-cloud-storage-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+  <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   <name>Storage Parent</name>
   <url>https://github.com/googleapis/java-storage</url>
   <description>
@@ -54,7 +54,7 @@
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <github.global.server>github</github.global.server>
     <site.installationModule>google-cloud-storage-parent</site.installationModule>
-    <google.cloud.shared-dependencies.version>3.25.0</google.cloud.shared-dependencies.version>
+    <google.cloud.shared-dependencies.version>3.31.0</google.cloud.shared-dependencies.version>
   </properties>
 
   <dependencyManagement>
@@ -65,10 +65,61 @@
         <version>${google-cloud-shared-dependencies.version}</version>
         <type>pom</type>
         <scope>import</scope>
+      </dependency>
+      <!--
+      Each of the following dependencies declares a dependency on opentelemtry-semconv,
+      but they differ in what version. Explicitly declare the version here to break the tie.
+      * com.google.cloud.opentelemetry:exporter-metrics
+      * com.google.cloud.opentelemetry:detector-resources
+      * io.opentelemetry.instrumentation:opentelemetry-resources
+      * io.opentelemetry.contrib:opentelemetry-gcp-resources
+      -->
+      <dependency>
+        <groupId>io.opentelemetry.semconv</groupId>
+        <artifactId>opentelemetry-semconv</artifactId>
+        <version>1.25.0-alpha</version>
+      </dependency>
+      <dependency>
+        <groupId>com.google.cloud.opentelemetry</groupId>
+        <artifactId>exporter-metrics</artifactId>
+        <version>0.31.0</version>
+        <exclusions>
+          <exclusion>
+            <groupId>io.opentelemetry.semconv</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>com.google.cloud.opentelemetry</groupId>
+        <artifactId>detector-resources</artifactId>
+        <version>0.27.0-alpha</version>
+        <exclusions>
+          <exclusion>
+            <groupId>io.opentelemetry.semconv</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>io.opentelemetry.instrumentation</groupId>
+        <artifactId>opentelemetry-resources</artifactId>
+        <version>2.6.0-alpha</version>
+        <exclusions>
+          <exclusion>
+            <groupId>io.opentelemetry.semconv</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>io.opentelemetry.contrib</groupId>
+        <artifactId>opentelemetry-gcp-resources</artifactId>
+        <version>1.37.0-alpha</version>
         <exclusions>
           <exclusion>
-            <groupId>io.grpc</groupId>
-            <artifactId>*</artifactId>
+            <groupId>io.opentelemetry.semconv</groupId>
+            <artifactId>opentelemetry-semconv</artifactId>
           </exclusion>
         </exclusions>
       </dependency>
@@ -76,7 +127,7 @@
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+        <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.apis</groupId>
@@ -98,32 +149,32 @@
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>gapic-google-cloud-storage-v2</artifactId>
-        <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
+        <version>2.41.0-alpha</version><!-- {x-version-update:grpc-google-cloud-storage-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>grpc-google-cloud-storage-control-v2</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
+        <version>2.41.0</version><!-- {x-version-update:grpc-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.api.grpc</groupId>
         <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+        <version>2.41.0</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
         <artifactId>google-cloud-storage-control</artifactId>
-        <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+        <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       </dependency>
       <dependency>
         <groupId>com.google.cloud</groupId>
diff --git a/proto-google-cloud-storage-control-v2/pom.xml b/proto-google-cloud-storage-control-v2/pom.xml
index ff2abbc9..b246d36a 100644
--- a/proto-google-cloud-storage-control-v2/pom.xml
+++ b/proto-google-cloud-storage-control-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-control-v2</artifactId>
-  <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
+  <version>2.41.0</version><!-- {x-version-update:proto-google-cloud-storage-control-v2:current} -->
   <name>proto-google-cloud-storage-control-v2</name>
   <description>Proto library for  proto-google-cloud-storage-control-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/proto-google-cloud-storage-v2/pom.xml b/proto-google-cloud-storage-v2/pom.xml
index 193645a5..c5f35483 100644
--- a/proto-google-cloud-storage-v2/pom.xml
+++ b/proto-google-cloud-storage-v2/pom.xml
@@ -4,13 +4,13 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>com.google.api.grpc</groupId>
   <artifactId>proto-google-cloud-storage-v2</artifactId>
-  <version>2.40.2-alpha-SNAPSHOT</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
+  <version>2.41.0-alpha</version><!-- {x-version-update:proto-google-cloud-storage-v2:current} -->
   <name>proto-google-cloud-storage-v2</name>
   <description>PROTO library for proto-google-cloud-storage-v2</description>
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
   <dependencies>
     <dependency>
diff --git a/renovate.json b/renovate.json
index c7467c6b..42380a2b 100644
--- a/renovate.json
+++ b/renovate.json
@@ -111,6 +111,16 @@
       ],
       "semanticCommitType": "test",
       "semanticCommitScope": "deps"
+    },
+    {
+      "groupName": "OpenTelemetry extended dependencies",
+      "packagePatterns": [
+        "^io.opentelemetry.semconv:",
+        "^io.opentelemetry.instrumentation:",
+        "^io.opentelemetry.contrib:",
+        "^com.google.cloud.opentelemetry:",
+        "$com.google.cloud.opentelemetry:shared-resourcemapping"
+      ]
     }
   ],
   "semanticCommits": true,
diff --git a/samples/install-without-bom/pom.xml b/samples/install-without-bom/pom.xml
index d78aef52..d17cc746 100644
--- a/samples/install-without-bom/pom.xml
+++ b/samples/install-without-bom/pom.xml
@@ -35,7 +35,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage-control</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
     <!-- [END storage_install_without_bom] -->
 
diff --git a/samples/native-image-sample/pom.xml b/samples/native-image-sample/pom.xml
index e25c7e10..2e15428c 100644
--- a/samples/native-image-sample/pom.xml
+++ b/samples/native-image-sample/pom.xml
@@ -67,7 +67,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage-control</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       <scope>compile</scope>
     </dependency>
   </dependencies>
diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml
index 848c09df..fd499ab6 100644
--- a/samples/snapshot/pom.xml
+++ b/samples/snapshot/pom.xml
@@ -28,12 +28,12 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
     </dependency>
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage-control</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage-control:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage-control:current} -->
       <scope>compile</scope>
     </dependency>
 
diff --git a/storage-shared-benchmarking/pom.xml b/storage-shared-benchmarking/pom.xml
index d51510c2..2dba9896 100644
--- a/storage-shared-benchmarking/pom.xml
+++ b/storage-shared-benchmarking/pom.xml
@@ -10,7 +10,7 @@
   <parent>
     <groupId>com.google.cloud</groupId>
     <artifactId>google-cloud-storage-parent</artifactId>
-    <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+    <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
   </parent>
 
   <properties>
@@ -31,7 +31,7 @@
     <dependency>
       <groupId>com.google.cloud</groupId>
       <artifactId>google-cloud-storage</artifactId>
-      <version>2.40.2-SNAPSHOT</version><!-- {x-version-update:google-cloud-storage:current} -->
+      <version>2.41.0</version><!-- {x-version-update:google-cloud-storage:current} -->
       <classifier>tests</classifier>
     </dependency>
     <dependency>
diff --git a/versions.txt b/versions.txt
index f69020a2..fdc80274 100644
--- a/versions.txt
+++ b/versions.txt
@@ -1,10 +1,10 @@
 # Format:
 # module:released-version:current-version
 
-google-cloud-storage:2.40.1:2.40.2-SNAPSHOT
-gapic-google-cloud-storage-v2:2.40.1-alpha:2.40.2-alpha-SNAPSHOT
-grpc-google-cloud-storage-v2:2.40.1-alpha:2.40.2-alpha-SNAPSHOT
-proto-google-cloud-storage-v2:2.40.1-alpha:2.40.2-alpha-SNAPSHOT
-google-cloud-storage-control:2.40.1:2.40.2-SNAPSHOT
-proto-google-cloud-storage-control-v2:2.40.1:2.40.2-SNAPSHOT
-grpc-google-cloud-storage-control-v2:2.40.1:2.40.2-SNAPSHOT
+google-cloud-storage:2.41.0:2.41.0
+gapic-google-cloud-storage-v2:2.41.0-alpha:2.41.0-alpha
+grpc-google-cloud-storage-v2:2.41.0-alpha:2.41.0-alpha
+proto-google-cloud-storage-v2:2.41.0-alpha:2.41.0-alpha
+google-cloud-storage-control:2.41.0:2.41.0
+proto-google-cloud-storage-control-v2:2.41.0:2.41.0
+grpc-google-cloud-storage-control-v2:2.41.0:2.41.0


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultRetryHandlingBehaviorTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultRetryHandlingBehaviorTest.java
index f1c37b09..0f90c752 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultRetryHandlingBehaviorTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/DefaultRetryHandlingBehaviorTest.java
@@ -35,6 +35,7 @@ import com.google.gson.stream.MalformedJsonException;
 import java.io.IOException;
 import java.net.SocketException;
 import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
 import java.security.cert.CertificateException;
 import java.util.Arrays;
 import java.util.Collection;
@@ -342,6 +343,7 @@ public final class DefaultRetryHandlingBehaviorTest {
     IO_EXCEPTION(new IOException("no retry")),
     AUTH_RETRYABLE_TRUE(new RetryableException(true)),
     AUTH_RETRYABLE_FALSE(new RetryableException(false)),
+    UNKNOWN_HOST_EXCEPTION(C.UNKNOWN_HOST_EXCEPTION),
     ;
 
     private final Throwable throwable;
@@ -415,6 +417,8 @@ public final class DefaultRetryHandlingBehaviorTest {
       private static final MalformedJsonException GSON_MALFORMED_EXCEPTION =
           new MalformedJsonException("parse-exception");
       private static final IOException IO_PREMATURE_EOF = new IOException("Premature EOF");
+      private static final UnknownHostException UNKNOWN_HOST_EXCEPTION =
+          new UnknownHostException("fake.fake");
 
       private static HttpResponseException newHttpResponseException(
           int httpStatusCode, String name) {
@@ -1065,6 +1069,16 @@ public final class DefaultRetryHandlingBehaviorTest {
                 ThrowableCategory.AUTH_RETRYABLE_FALSE,
                 HandlerCategory.NONIDEMPOTENT,
                 ExpectRetry.NO,
+                Behavior.SAME),
+            new Case(
+                ThrowableCategory.UNKNOWN_HOST_EXCEPTION,
+                HandlerCategory.IDEMPOTENT,
+                ExpectRetry.YES,
+                Behavior.DEFAULT_MORE_PERMISSIBLE),
+            new Case(
+                ThrowableCategory.UNKNOWN_HOST_EXCEPTION,
+                HandlerCategory.NONIDEMPOTENT,
+                ExpectRetry.NO,
                 Behavior.SAME))
         .build();
   }
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGrpcMetricsTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGrpcMetricsTest.java
new file mode 100644
index 00000000..f1a406ca
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ITGrpcMetricsTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.cloud.storage.it.runner.StorageITRunner;
+import com.google.cloud.storage.it.runner.annotations.Backend;
+import com.google.cloud.storage.it.runner.annotations.SingleBackend;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(StorageITRunner.class)
+@SingleBackend(Backend.PROD)
+public class ITGrpcMetricsTest {
+  @Test
+  public void testGrpcMetrics() {
+    GrpcStorageOptions grpcStorageOptions = StorageOptions.grpc().build();
+    assertThat(
+            OpenTelemetryBootstrappingUtils.getCloudMonitoringEndpoint(
+                "storage.googleapis.com:443", "storage.googleapis.com"))
+        .isEqualTo("monitoring.googleapis.com:443");
+
+    SdkMeterProvider provider =
+        OpenTelemetryBootstrappingUtils.createMeterProvider(
+            "monitoring.googleapis.com:443", grpcStorageOptions.getProjectId(), false);
+
+    /*
+     * SDKMeterProvider doesn't expose the relevant fields we want to test, but they are present in
+     * the String representation, so we'll check that instead. Most of the resources are auto-set,
+     * and will depend on environment, which could cause flakes to check. We're only responsible for
+     * setting the project ID, endpoint, and Histogram boundaries, so we'll just check those
+     */
+    String result = provider.toString();
+
+    // What the project ID will be will depend on the environment, so we just make sure it's present
+    // and not null/empty
+    assertThat(result.contains("project_id"));
+    assertThat(result).doesNotContain("project_id=\"\"");
+    assertThat(result).doesNotContain("project_id=null");
+
+    // This is the check for the Seconds histogram boundary. We can't practically check for every
+    // boundary,
+    // but if *any* are present, that means they're different from the results and we successfully
+    // set them
+    assertThat(result).contains("1.2");
+
+    // This is the check for the Size boundary
+    assertThat(result).contains("131072");
+  }
+
+  @Test
+  public void testGrpcMetrics_universeDomain() {
+    assertThat("monitoring.my-universe-domain.com:443")
+        .isEqualTo(
+            OpenTelemetryBootstrappingUtils.getCloudMonitoringEndpoint(
+                "storage.my-universe-domain.com:443", "my-universe-domain.com"));
+  }
+
+  @Test
+  public void testGrpcMetrics_private() {
+    assertThat("private.googleapis.com:443")
+        .isEqualTo(
+            OpenTelemetryBootstrappingUtils.getCloudMonitoringEndpoint(
+                "private.googleapis.com:443", null));
+  }
+
+  @Test
+  public void testGrpcMetrics_restricted() {
+    assertThat("restricted.googleapis.com:443")
+        .isEqualTo(
+            OpenTelemetryBootstrappingUtils.getCloudMonitoringEndpoint(
+                "restricted.googleapis.com:443", null));
+  }
+}