Skip to content

Commit 801f77e

Browse files
Try resolving project id from Google credentials if not provided
1 parent a63bba4 commit 801f77e

File tree

4 files changed

+171
-8
lines changed

4 files changed

+171
-8
lines changed

gcp-auth-extension/README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,13 @@ The extension can be configured either by environment variables or system proper
3434

3535
Here is a list of required and optional configuration available for the extension:
3636

37-
#### Required Config
37+
#### Optional Config
3838

3939
- `GOOGLE_CLOUD_PROJECT`: Environment variable that represents the Google Cloud Project ID to which the telemetry needs to be exported.
4040

4141
- Can also be configured using `google.cloud.project` system property.
42-
- This is a required option, the agent configuration will fail if this option is not set.
43-
44-
#### Optional Config
42+
- If neither of these options are set, the extension will attempt to infer the project id from the current credentials as a fallback, however notice that not all credentials implementations will be able to provide a project id, so the inference is only a best-effort attempt.
43+
- **Important Note**: The agent configuration will fail if this option is not set and cannot be inferred.
4544

4645
- `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.
4746

gcp-auth-extension/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies {
3131
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
3232
testImplementation("org.junit.jupiter:junit-jupiter-api")
3333
testCompileOnly("org.junit.jupiter:junit-jupiter-params")
34+
testImplementation("org.junit-pioneer:junit-pioneer")
3435

3536
testImplementation("io.opentelemetry:opentelemetry-api")
3637
testImplementation("io.opentelemetry:opentelemetry-exporter-otlp")

gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) {
111111
.addMetricExporterCustomizer(
112112
(metricExporter, configProperties) ->
113113
customizeMetricExporter(metricExporter, credentials, configProperties))
114-
.addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource);
114+
.addResourceCustomizer(
115+
(resource, configProperties) ->
116+
customizeResource(resource, credentials, configProperties));
115117
}
116118

117119
@Override
@@ -228,9 +230,19 @@ private static Map<String, String> getRequiredHeaderMap(
228230
}
229231

230232
// Updates the current resource with the attributes required for ingesting OTLP data on GCP.
231-
private static Resource customizeResource(Resource resource, ConfigProperties configProperties) {
232-
String gcpProjectId =
233-
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties);
233+
// Note that credentials can be passed from `customize` function directly
234+
private static Resource customizeResource(
235+
Resource resource, GoogleCredentials credentials, ConfigProperties configProperties) {
236+
String gcpProjectId;
237+
try {
238+
gcpProjectId = ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties);
239+
} catch (io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException e) {
240+
gcpProjectId = credentials.getProjectId();
241+
if (gcpProjectId == null) {
242+
// this exception will still contain the accurate message.
243+
throw e;
244+
}
245+
}
234246
Resource res = Resource.create(Attributes.of(stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId));
235247
return resource.merge(res);
236248
}

gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import org.junit.jupiter.params.ParameterizedTest;
7575
import org.junit.jupiter.params.provider.Arguments;
7676
import org.junit.jupiter.params.provider.MethodSource;
77+
import org.junitpioneer.jupiter.ClearSystemProperty;
7778
import org.mockito.ArgumentCaptor;
7879
import org.mockito.Captor;
7980
import org.mockito.Mock;
@@ -456,6 +457,92 @@ void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws IOExce
456457
}
457458
}
458459

460+
@ParameterizedTest
461+
@MethodSource("provideProjectIdBehaviorTestCases")
462+
@ClearSystemProperty(key = "google.cloud.project")
463+
@ClearSystemProperty(key = "google.otel.auth.target.signals")
464+
@SuppressWarnings("CannotMockMethod")
465+
void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws IOException {
466+
467+
// configure environment according to test case
468+
String userSpecifiedProjectId = testCase.getUserSpecifiedProjectId();
469+
if (userSpecifiedProjectId != null && !userSpecifiedProjectId.isEmpty()) {
470+
System.setProperty(
471+
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), userSpecifiedProjectId);
472+
}
473+
System.setProperty(
474+
ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES);
475+
476+
// prepare request metadata (may or may not be called depending on test scenario)
477+
AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now()));
478+
ImmutableMap<String, List<String>> mockedRequestMetadata =
479+
ImmutableMap.of(
480+
"Authorization",
481+
Collections.singletonList("Bearer " + fakeAccessToken.getTokenValue()));
482+
Mockito.lenient()
483+
.when(mockedGoogleCredentials.getRequestMetadata())
484+
.thenReturn(mockedRequestMetadata);
485+
486+
// only mock getProjectId() if it will be called (i.e., user didn't specify project ID)
487+
boolean shouldFallbackToCredentials =
488+
userSpecifiedProjectId == null || userSpecifiedProjectId.isEmpty();
489+
if (shouldFallbackToCredentials) {
490+
Mockito.when(mockedGoogleCredentials.getProjectId())
491+
.thenReturn(testCase.getCredentialsProjectId());
492+
}
493+
494+
// prepare mock exporter
495+
OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class);
496+
OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
497+
Mockito.spy(OtlpGrpcSpanExporter.builder());
498+
List<SpanData> exportedSpans = new ArrayList<>();
499+
configureGrpcMockSpanExporter(
500+
mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans);
501+
502+
try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
503+
Mockito.mockStatic(GoogleCredentials.class)) {
504+
googleCredentialsMockedStatic
505+
.when(GoogleCredentials::getApplicationDefault)
506+
.thenReturn(mockedGoogleCredentials);
507+
508+
if (testCase.getExpectedToThrow()) {
509+
// expect exception to be thrown when project ID is not available
510+
assertThatThrownBy(
511+
() -> {
512+
OpenTelemetrySdk sdk =
513+
buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
514+
generateTestSpan(sdk);
515+
sdk.shutdown().join(10, TimeUnit.SECONDS);
516+
})
517+
.isInstanceOf(ConfigurationException.class);
518+
// verify getProjectId() was called to attempt fallback
519+
Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId();
520+
} else {
521+
// export telemetry and verify resource attributes contain expected project ID
522+
OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
523+
generateTestSpan(sdk);
524+
CompletableResultCode code = sdk.shutdown();
525+
CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
526+
assertThat(joinResult.isSuccess()).isTrue();
527+
528+
assertThat(exportedSpans).hasSizeGreaterThan(0);
529+
for (SpanData spanData : exportedSpans) {
530+
assertThat(spanData.getResource().getAttributes().asMap())
531+
.containsEntry(
532+
AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY),
533+
testCase.getExpectedProjectIdInResource());
534+
}
535+
536+
// verify whether getProjectId() was called based on whether fallback was needed
537+
if (shouldFallbackToCredentials) {
538+
Mockito.verify(mockedGoogleCredentials, Mockito.times(1)).getProjectId();
539+
} else {
540+
Mockito.verify(mockedGoogleCredentials, Mockito.never()).getProjectId();
541+
}
542+
}
543+
}
544+
}
545+
459546
@ParameterizedTest
460547
@MethodSource("provideTargetSignalBehaviorTestCases")
461548
void testTargetSignalsBehavior(TargetSignalBehavior testCase) {
@@ -680,6 +767,34 @@ private static Stream<Arguments> provideTargetSignalBehaviorTestCases() {
680767
* indicates that the mocked credentials are configured to provide DUMMY_GCP_QUOTA_PROJECT_ID as
681768
* the quota project ID.
682769
*/
770+
private static Stream<Arguments> provideProjectIdBehaviorTestCases() {
771+
return Stream.of(
772+
// User specified project ID takes precedence
773+
Arguments.of(
774+
ProjectIdTestBehavior.builder()
775+
.setUserSpecifiedProjectId(DUMMY_GCP_RESOURCE_PROJECT_ID)
776+
.setCredentialsProjectId("credentials-project-id")
777+
.setExpectedProjectIdInResource(DUMMY_GCP_RESOURCE_PROJECT_ID)
778+
.setExpectedToThrow(false)
779+
.build()),
780+
// If user doesn't specify project ID, fallback to credentials.getProjectId()
781+
Arguments.of(
782+
ProjectIdTestBehavior.builder()
783+
.setUserSpecifiedProjectId(null)
784+
.setCredentialsProjectId("credentials-project-id")
785+
.setExpectedProjectIdInResource("credentials-project-id")
786+
.setExpectedToThrow(false)
787+
.build()),
788+
// If user doesn't specify and credentials.getProjectId() returns null, throw exception
789+
Arguments.of(
790+
ProjectIdTestBehavior.builder()
791+
.setUserSpecifiedProjectId(null)
792+
.setCredentialsProjectId(null)
793+
.setExpectedProjectIdInResource(null)
794+
.setExpectedToThrow(true)
795+
.build()));
796+
}
797+
683798
private static Stream<Arguments> provideQuotaBehaviorTestCases() {
684799
return Stream.of(
685800
// If quota project present in metadata, it will be used
@@ -839,6 +954,42 @@ private static void configureGrpcMockMetricExporter(
839954
.thenReturn(MemoryMode.IMMUTABLE_DATA);
840955
}
841956

957+
@AutoValue
958+
abstract static class ProjectIdTestBehavior {
959+
// A null user specified project ID represents the use case where user omits specifying it
960+
@Nullable
961+
abstract String getUserSpecifiedProjectId();
962+
963+
// The project ID that credentials.getProjectId() returns (can be null)
964+
@Nullable
965+
abstract String getCredentialsProjectId();
966+
967+
// The expected project ID in the resource attributes (null if exception expected)
968+
@Nullable
969+
abstract String getExpectedProjectIdInResource();
970+
971+
// Whether an exception is expected to be thrown
972+
abstract boolean getExpectedToThrow();
973+
974+
static Builder builder() {
975+
return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_ProjectIdTestBehavior
976+
.Builder();
977+
}
978+
979+
@AutoValue.Builder
980+
abstract static class Builder {
981+
abstract Builder setUserSpecifiedProjectId(String projectId);
982+
983+
abstract Builder setCredentialsProjectId(String projectId);
984+
985+
abstract Builder setExpectedProjectIdInResource(String projectId);
986+
987+
abstract Builder setExpectedToThrow(boolean expectedToThrow);
988+
989+
abstract ProjectIdTestBehavior build();
990+
}
991+
}
992+
842993
@AutoValue
843994
abstract static class QuotaProjectIdTestBehavior {
844995
// A null user specified quota represents the use case where user omits specifying quota

0 commit comments

Comments
 (0)