diff --git a/google-cloud-storage/pom.xml b/google-cloud-storage/pom.xml
index b6bc155e3d..839cc39559 100644
--- a/google-cloud-storage/pom.xml
+++ b/google-cloud-storage/pom.xml
@@ -124,6 +124,53 @@
com.google.api.grpc
gapic-google-cloud-storage-v2
+
+
+ io.opentelemetry
+ opentelemetry-sdk
+
+
+ io.grpc
+ grpc-opentelemetry
+
+
+ io.opentelemetry
+ opentelemetry-api
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-metrics
+
+
+ io.opentelemetry
+ opentelemetry-sdk-common
+
+
+
+ io.opentelemetry
+ opentelemetry-sdk-extension-autoconfigure-spi
+
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+
+
+
+ com.google.cloud.opentelemetry
+ exporter-metrics
+
+
+
+ io.opentelemetry.contrib
+ opentelemetry-gcp-resources
+
+
+
+ org.checkerframework
+ checker-qual
+
@@ -159,10 +206,7 @@
grpc-googleapis
runtime
-
- org.checkerframework
- checker-qual
-
+
org.junit.vintage:junit-vintage-engine
+
+
+ io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi
+ io.opentelemetry.semconv:opentelemetry-semconv
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 92081e03e0..64376cc107 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 @@ private GrpcStorageOptions(Builder builder, GrpcStorageDefaults serviceDefaults)
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 @@ private Tuple> resolveSettingsAndOpts() throw
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 int hashCode() {
retryAlgorithmManager,
terminationAwaitDuration,
attemptDirectPath,
+ enableGrpcClientMetrics,
grpcInterceptorProvider,
blobWriteSessionConfig,
baseHashCode());
@@ -365,6 +381,7 @@ public boolean equals(Object o) {
}
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 static final class Builder extends StorageOptions.Builder {
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 static final class Builder extends StorageOptions.Builder {
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 GrpcStorageOptions.Builder setAttemptDirectPath(boolean attemptDirectPath
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 boolean isAttemptDirectPath() {
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 0000000000..ed0ba544e5
--- /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 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 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 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 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 latencyHistogramBoundaries() {
+ List 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 sizeHistogramBoundaries() {
+ long kb = 1024;
+ long mb = 1024 * kb;
+ long gb = 1024 * mb;
+
+ List 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 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/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 0000000000..f1a406ca6c
--- /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));
+ }
+}
diff --git a/pom.xml b/pom.xml
index 6087fd2918..4778468747 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,7 +54,7 @@
UTF-8
github
google-cloud-storage-parent
- 3.25.0
+ 3.31.0
@@ -65,10 +65,61 @@
${google-cloud-shared-dependencies.version}
pom
import
+
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+ 1.25.0-alpha
+
+
+ com.google.cloud.opentelemetry
+ exporter-metrics
+ 0.31.0
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+
+
+
+
+ com.google.cloud.opentelemetry
+ detector-resources
+ 0.27.0-alpha
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+
+
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-resources
+ 2.6.0-alpha
+
+
+ io.opentelemetry.semconv
+ opentelemetry-semconv
+
+
+
+
+ io.opentelemetry.contrib
+ opentelemetry-gcp-resources
+ 1.37.0-alpha
- io.grpc
- *
+ io.opentelemetry.semconv
+ opentelemetry-semconv
diff --git a/renovate.json b/renovate.json
index c7467c6b72..42380a2b69 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,