Skip to content

Commit 95a41e5

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

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 credentials used, 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)