From 4318e3524e429c7abb9187239ea676b75d0072b1 Mon Sep 17 00:00:00 2001 From: Gustavo Bastos Date: Thu, 14 Aug 2025 16:16:22 +0200 Subject: [PATCH] Try resolving GCP_PROJECT with ServiceOptions if not provided --- gcp-auth-extension/README.md | 7 ++- gcp-auth-extension/build.gradle.kts | 1 + ...thAutoConfigurationCustomizerProvider.java | 38 ++++++++++++++-- ...toConfigurationCustomizerProviderTest.java | 44 +++++++++++++++++++ 4 files changed, 82 insertions(+), 8 deletions(-) diff --git a/gcp-auth-extension/README.md b/gcp-auth-extension/README.md index 209283daa..e1ac5bcf2 100644 --- a/gcp-auth-extension/README.md +++ b/gcp-auth-extension/README.md @@ -34,14 +34,13 @@ The extension can be configured either by environment variables or system proper Here is a list of required and optional configuration available for the extension: -#### Required Config +#### Optional Config - `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported. - Can also be configured using `google.cloud.project` system property. - - This is a required option, the agent configuration will fail if this option is not set. - -#### Optional Config + - If neither of these options are set, the extension will attempt to use the `ServiceOptions` class from `google-cloud-core` to determine the project ID. The `ServiceOptions` has a comprehensive logic to determine the project ID, which includes checking the environment variable `GOOGLE_CLOUD_PROJECT`, the `gcloud` configuration, the local metadata server, and the `GOOGLE_APPLICATION_CREDENTIALS` file. + - **Important Note**: The agent configuration will fail if this option is not set or cannot be inferred. - `GOOGLE_CLOUD_QUOTA_PROJECT`: Environment variable that represents the Google Cloud Quota Project ID which will be charged for the GCP API usage. To learn more about a *quota project*, see the [Quota project overview](https://cloud.google.com/docs/quotas/quota-project) page. Additional details about configuring the *quota project* can be found on the [Set the quota project](https://cloud.google.com/docs/quotas/set-quota-project) page. diff --git a/gcp-auth-extension/build.gradle.kts b/gcp-auth-extension/build.gradle.kts index f81e5e521..231edc34d 100644 --- a/gcp-auth-extension/build.gradle.kts +++ b/gcp-auth-extension/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { // Only dependencies added to `implementation` configuration will be picked up by Shadow plugin implementation("com.google.auth:google-auth-library-oauth2-http:1.37.1") + implementation("com.google.cloud:google-cloud-core:2.58.1") // Test dependencies testCompileOnly("com.google.auto.service:auto-service-annotations") diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 1de583029..c39d8a929 100644 --- a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -7,6 +7,7 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.auto.service.AutoService; +import com.google.cloud.ServiceOptions; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.contrib.gcp.auth.GoogleAuthException.Reason; @@ -21,6 +22,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -109,7 +111,12 @@ public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { .addMetricExporterCustomizer( (metricExporter, configProperties) -> customizeMetricExporter(metricExporter, credentials, configProperties)) - .addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource); + .addResourceCustomizer( + (resource, configProperties) -> { + String gcpProjectId = getGoogleProjectId(configProperties); + + return customizeResource(resource, gcpProjectId); + }); } @Override @@ -227,12 +234,35 @@ private static Map getRequiredHeaderMap( } // Updates the current resource with the attributes required for ingesting OTLP data on GCP. - private static Resource customizeResource(Resource resource, ConfigProperties configProperties) { - String gcpProjectId = - ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties); + private static Resource customizeResource(Resource resource, String gcpProjectId) { Resource res = Resource.create( Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId)); return resource.merge(res); } + + /** + * Retrieves the Google Cloud Project ID from the configuration properties, falling back to + * google-cloud-core's ServiceOptions project ID resolution if not explicitly set. + * + * @param configProperties The configuration properties containing the GCP project ID. + * @return The Google Cloud Project ID. + */ + @Nonnull + static String getGoogleProjectId(ConfigProperties configProperties) { + String googleProjectId = + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValueWithFallback( + configProperties, ServiceOptions::getDefaultProjectId); + + if (googleProjectId == null || googleProjectId.isEmpty()) { + throw new ConfigurationException( + String.format( + "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s", + ConfigurableOption.GOOGLE_CLOUD_PROJECT, + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getEnvironmentVariable(), + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty())); + } + + return googleProjectId; + } } diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java index 4fb687925..3e873d703 100644 --- a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java @@ -39,6 +39,7 @@ import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; import io.opentelemetry.sdk.common.CompletableResultCode; @@ -538,6 +539,49 @@ public void testTargetSignalsBehavior(TargetSignalBehavior testCase) { } } + @Test + void testThrowsExceptionIfGoogleProjectIsNotFound() { + // Clear the system property to ensure it does not interfere with the test + System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); + System.clearProperty("GOOGLE_CLOUD_PROJECT"); + + DefaultConfigProperties configProperties = + DefaultConfigProperties.create(Collections.emptyMap(), Mockito.mock(ComponentLoader.class)); + + assertThrows( + ConfigurationException.class, + () -> GcpAuthAutoConfigurationCustomizerProvider.getGoogleProjectId(configProperties)); + } + + @Test + void testResolveGoogleProjectIdFromSystemProperty() { + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + + DefaultConfigProperties configProperties = + DefaultConfigProperties.create(Collections.emptyMap(), Mockito.mock(ComponentLoader.class)); + + assertEquals( + DUMMY_GCP_RESOURCE_PROJECT_ID, + GcpAuthAutoConfigurationCustomizerProvider.getGoogleProjectId(configProperties)); + } + + @Test + void testResolveGoogleProjectIdFromServiceOptions() { + // Clear the system property to ensure it does not interfere with the test + System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty()); + // Property that the extension expects is google.cloud.project, ServiceOptions works with + // GOOGLE_CLOUD_PROJECT, so this will highlight the fallback to ServiceOptions + System.setProperty("GOOGLE_CLOUD_PROJECT", DUMMY_GCP_RESOURCE_PROJECT_ID); + + DefaultConfigProperties configProperties = + DefaultConfigProperties.create(Collections.emptyMap(), Mockito.mock(ComponentLoader.class)); + + assertEquals( + DUMMY_GCP_RESOURCE_PROJECT_ID, + GcpAuthAutoConfigurationCustomizerProvider.getGoogleProjectId(configProperties)); + } + /** Test cases specifying expected behavior for GOOGLE_OTEL_AUTH_TARGET_SIGNALS */ private static Stream provideTargetSignalBehaviorTestCases() { return Stream.of(