Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion gcp-auth-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ Here is a list of configurable options for the extension:

- `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.
- If this option is not configured, the extension would infer GCP Project ID from the application default credentials. For more information on application default credentials, see [here](https://cloud.google.com/docs/authentication/application-default-credentials).
- This is a required option, the agent configuration will fail if this option is not set.
- `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 [here](https://cloud.google.com/docs/quotas/quota-project).
- Can also be configured using `google.cloud.quota.project` system property.
- If this option is not configured, the extension would infer GCP Quota Project ID from the application default credentials. For more information on application default credentials, see [here](https://cloud.google.com/docs/authentication/application-default-credentials).

## Usage

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ public enum ConfigurableOption {
* Represents the Google Cloud Project ID option. Can be configured using the environment variable
* `GOOGLE_CLOUD_PROJECT` or the system property `google.cloud.project`.
*/
GOOGLE_CLOUD_PROJECT("Google Cloud Project ID");
GOOGLE_CLOUD_PROJECT("Google Cloud Project ID"),

/**
* Represents the Google Cloud Quota Project ID option. Can be configured using the environment
* variable `GOOGLE_CLOUD_QUOTA_PROJECT` or the system property `google.cloud.quota.project`. The
* quota project is the project that is used for quota management and billing for the API usage.
*
* <p>The environment variable name is selected to be consistent with the <a
* href="https://cloud.google.com/docs/quotas/set-quota-project">official GCP client
* libraries</a>.
*/
GOOGLE_CLOUD_QUOTA_PROJECT("Google Cloud Quota Project ID");

private final String userReadableName;
private final String environmentVariableName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,16 @@ public class GcpAuthAutoConfigurationCustomizerProvider
*/
@Override
public void customize(AutoConfigurationCustomizer autoConfiguration) {
GoogleCredentials credentials;
try {
GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
autoConfiguration
.addSpanExporterCustomizer(
(exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials))
.addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource);
credentials = GoogleCredentials.getApplicationDefault();
} catch (IOException e) {
throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e);
}
autoConfiguration
.addSpanExporterCustomizer(
(exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials))
.addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource);
}

@Override
Expand Down Expand Up @@ -100,24 +101,19 @@ private static Map<String, String> getRequiredHeaderMap(GoogleCredentials creden
} catch (IOException e) {
throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e);
}
gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, credentials.getQuotaProjectId());
gcpHeaders.put("Authorization", "Bearer " + credentials.getAccessToken().getTokenValue());
String configuredQuotaProjectId =
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueWithFallback(
credentials::getQuotaProjectId);
if (configuredQuotaProjectId != null && !configuredQuotaProjectId.isEmpty()) {
gcpHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId);
}
return gcpHeaders;
}

// 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.getConfiguredValueWithFallback(
() -> {
try {
GoogleCredentials googleCredentials = GoogleCredentials.getApplicationDefault();
return googleCredentials.getQuotaProjectId();
} catch (IOException e) {
throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e);
}
});

String gcpProjectId = ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue();
Resource res =
Resource.create(
Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.google.auth.oauth2.AccessToken;
Expand All @@ -28,6 +29,7 @@
import io.opentelemetry.sdk.autoconfigure.internal.ComponentLoader;
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.traces.ConfigurableSpanExporterProvider;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
Expand Down Expand Up @@ -58,6 +60,9 @@
@ExtendWith(MockitoExtension.class)
class GcpAuthAutoConfigurationCustomizerProviderTest {

private static final String DUMMY_GCP_RESOURCE_PROJECT_ID = "my-gcp-resource-project-id";
private static final String DUMMY_GCP_QUOTA_PROJECT_ID = "my-gcp-quota-project-id";

@Mock private GoogleCredentials mockedGoogleCredentials;

@Captor private ArgumentCaptor<Supplier<Map<String, String>>> headerSupplierCaptor;
Expand All @@ -74,16 +79,17 @@ class GcpAuthAutoConfigurationCustomizerProviderTest {
"foo=bar");

@BeforeEach
@SuppressWarnings("CannotMockMethod")
public void setup() {
MockitoAnnotations.openMocks(this);
Mockito.when(mockedGoogleCredentials.getQuotaProjectId()).thenReturn("test-project");
Mockito.when(mockedGoogleCredentials.getAccessToken())
.thenReturn(new AccessToken("fake", Date.from(Instant.now())));
}

@Test
public void testCustomizerOtlpHttp() {
// Set resource project system property
System.setProperty(
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID);
// Prepare mocks
prepareMockBehaviorForGoogleCredentials();
OtlpHttpSpanExporter mockOtlpHttpSpanExporter = Mockito.mock(OtlpHttpSpanExporter.class);
OtlpHttpSpanExporterBuilder otlpSpanExporterBuilder = OtlpHttpSpanExporter.builder();
OtlpHttpSpanExporterBuilder spyOtlpHttpSpanExporterBuilder =
Expand Down Expand Up @@ -116,7 +122,7 @@ public void testCustomizerOtlpHttp() {
Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1))
.setHeaders(headerSupplierCaptor.capture());
assertEquals(2, headerSupplierCaptor.getValue().get().size());
assertThat(verifyAuthHeaders(headerSupplierCaptor.getValue().get())).isTrue();
assertThat(authHeadersQuotaProjectIsPresent(headerSupplierCaptor.getValue().get())).isTrue();

Mockito.verify(mockOtlpHttpSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection());

Expand All @@ -125,7 +131,9 @@ public void testCustomizerOtlpHttp() {
.allSatisfy(
spanData -> {
assertThat(spanData.getResource().getAttributes().asMap())
.containsEntry(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), "test-project")
.containsEntry(
AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY),
DUMMY_GCP_RESOURCE_PROJECT_ID)
.containsEntry(AttributeKey.stringKey("foo"), "bar");
assertThat(spanData.getAttributes().asMap())
.containsKey(AttributeKey.longKey("work_loop"));
Expand All @@ -135,6 +143,11 @@ public void testCustomizerOtlpHttp() {

@Test
public void testCustomizerOtlpGrpc() {
// Set resource project system property
System.setProperty(
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID);
// Prepare mocks
prepareMockBehaviorForGoogleCredentials();
OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class);
OtlpGrpcSpanExporterBuilder otlpSpanExporterBuilder = OtlpGrpcSpanExporter.builder();
OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
Expand Down Expand Up @@ -166,7 +179,7 @@ public void testCustomizerOtlpGrpc() {
Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1))
.setHeaders(headerSupplierCaptor.capture());
assertEquals(2, headerSupplierCaptor.getValue().get().size());
verifyAuthHeaders(headerSupplierCaptor.getValue().get());
assertThat(authHeadersQuotaProjectIsPresent(headerSupplierCaptor.getValue().get())).isTrue();

Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection());

Expand All @@ -175,14 +188,39 @@ public void testCustomizerOtlpGrpc() {
.allSatisfy(
spanData -> {
assertThat(spanData.getResource().getAttributes().asMap())
.containsEntry(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), "test-project")
.containsEntry(
AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY),
DUMMY_GCP_RESOURCE_PROJECT_ID)
.containsEntry(AttributeKey.stringKey("foo"), "bar");
assertThat(spanData.getAttributes().asMap())
.containsKey(AttributeKey.longKey("work_loop"));
});
}
}

@Test
public void testCustomizerFailWithMissingResourceProject() {
OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class);
try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
Mockito.mockStatic(GoogleCredentials.class)) {
googleCredentialsMockedStatic
.when(GoogleCredentials::getApplicationDefault)
.thenReturn(mockedGoogleCredentials);

assertThrows(
ConfigurationException.class,
() -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter));
}
}

@SuppressWarnings("CannotMockMethod")
private void prepareMockBehaviorForGoogleCredentials() {
Mockito.when(mockedGoogleCredentials.getQuotaProjectId())
.thenReturn(DUMMY_GCP_QUOTA_PROJECT_ID);
Mockito.when(mockedGoogleCredentials.getAccessToken())
.thenReturn(new AccessToken("fake", Date.from(Instant.now())));
}

private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) {
SpiHelper spiHelper =
SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader());
Expand Down Expand Up @@ -215,9 +253,10 @@ public String getName() {
return builder.build().getOpenTelemetrySdk();
}

private static boolean verifyAuthHeaders(Map<String, String> headers) {
private static boolean authHeadersQuotaProjectIsPresent(Map<String, String> headers) {
Set<Entry<String, String>> headerEntrySet = headers.entrySet();
return headerEntrySet.contains(new SimpleEntry<>(QUOTA_USER_PROJECT_HEADER, "test-project"))
return headerEntrySet.contains(
new SimpleEntry<>(QUOTA_USER_PROJECT_HEADER, DUMMY_GCP_QUOTA_PROJECT_ID))
&& headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer fake"));
}

Expand Down
Loading