diff --git a/gcp-auth-extension/README.md b/gcp-auth-extension/README.md index bb2c32886..ba5e1be15 100644 --- a/gcp-auth-extension/README.md +++ b/gcp-auth-extension/README.md @@ -47,6 +47,10 @@ Here is a list of required and optional configuration available for the extensio - Can also be configured using `google.cloud.quota.project` system property. +- `GOOGLE_OTEL_AUTH_TARGET_SIGNALS`: Environment variable that specifies a comma-separated list of OpenTelemetry signals for which this authentication extension should be active. Valid values contain - `metrics`, `traces` or `all`. If left unspecified, `all` is assumed meaning the extension will attempt to apply authentication to exports for all signals. + + - Can also be configured using `google.otel.auth.target.signals` system property. + ## Usage ### With OpenTelemetry Java agent diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java index 7928f9ab4..5f4a627e1 100644 --- a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java @@ -30,7 +30,27 @@ public enum ConfigurableOption { * href="https://cloud.google.com/docs/quotas/set-quota-project">official GCP client * libraries. */ - GOOGLE_CLOUD_QUOTA_PROJECT("Google Cloud Quota Project ID"); + GOOGLE_CLOUD_QUOTA_PROJECT("Google Cloud Quota Project ID"), + + /** + * Specifies a comma-separated list of OpenTelemetry signals for which this authentication + * extension should be active. The authentication mechanisms provided by this extension will only + * be applied to the listed signals. If not set, {@code all} is assumed to be set which means + * authentication is enabled for all supported signals. + * + *

Valid signal values are: + * + *

+ * + *

The values are case-sensitive. Whitespace around commas and values is ignored. Can be + * configured using the environment variable `GOOGLE_OTEL_AUTH_TARGET_SIGNALS` or the system + * property `google.otel.auth.target.signals`. + */ + GOOGLE_OTEL_AUTH_TARGET_SIGNALS("Target Signals for Google Auth Extension"); private final String userReadableName; private final String environmentVariableName; 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 053aeef7d..d6c68e54f 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 @@ -10,21 +10,28 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.contrib.gcp.auth.GoogleAuthException.Reason; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; 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.metrics.export.MetricExporter; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import javax.annotation.Nonnull; /** * An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) OpenTelemetry (OTLP) @@ -46,13 +53,29 @@ public class GcpAuthAutoConfigurationCustomizerProvider static final String QUOTA_USER_PROJECT_HEADER = "x-goog-user-project"; static final String GCP_USER_PROJECT_ID_KEY = "gcp.project_id"; + static final String SIGNAL_TYPE_TRACES = "traces"; + static final String SIGNAL_TYPE_METRICS = "metrics"; + static final String SIGNAL_TYPE_ALL = "all"; + /** - * Customizes the provided {@link AutoConfigurationCustomizer}. + * Customizes the provided {@link AutoConfigurationCustomizer} such that authenticated exports to + * GCP Telemetry API are possible from the configured OTLP exporter. * *

This method attempts to retrieve Google Application Default Credentials (ADC) and performs - * the following: - Adds authorization headers to the configured {@link SpanExporter} based on the - * retrieved credentials. - Adds default properties for OTLP endpoint and resource attributes for - * GCP integration. + * the following: + * + *

+ * + * The 'customization' performed includes customizing the exporters by adding required headers to + * the export calls made and customizing the resource by adding required resource attributes to + * enable GCP integration. * * @param autoConfiguration the AutoConfigurationCustomizer to customize. * @throws GoogleAuthException if there's an error retrieving Google Application Default @@ -61,7 +84,7 @@ public class GcpAuthAutoConfigurationCustomizerProvider * not configured through environment variables or system properties. */ @Override - public void customize(AutoConfigurationCustomizer autoConfiguration) { + public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { GoogleCredentials credentials; try { credentials = GoogleCredentials.getApplicationDefault(); @@ -70,7 +93,10 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) { } autoConfiguration .addSpanExporterCustomizer( - (exporter, configProperties) -> addAuthorizationHeaders(exporter, credentials)) + (spanExporter, configProperties) -> customizeSpanExporter(spanExporter, credentials)) + .addMetricExporterCustomizer( + (metricExporter, configProperties) -> + customizeMetricExporter(metricExporter, credentials)) .addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource); } @@ -79,6 +105,34 @@ public int order() { return Integer.MAX_VALUE - 1; } + private static SpanExporter customizeSpanExporter( + SpanExporter exporter, GoogleCredentials credentials) { + if (isSignalTargeted(SIGNAL_TYPE_TRACES)) { + return addAuthorizationHeaders(exporter, credentials); + } + return exporter; + } + + private static MetricExporter customizeMetricExporter( + MetricExporter exporter, GoogleCredentials credentials) { + if (isSignalTargeted(SIGNAL_TYPE_METRICS)) { + return addAuthorizationHeaders(exporter, credentials); + } + return exporter; + } + + // Checks if the auth extension is configured to target the passed signal for authentication. + private static boolean isSignalTargeted(String checkSignal) { + String userSpecifiedTargetedSignals = + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( + () -> SIGNAL_TYPE_ALL); + return Arrays.stream(userSpecifiedTargetedSignals.split(",")) + .map(String::trim) + .anyMatch( + targetedSignal -> + targetedSignal.equals(checkSignal) || targetedSignal.equals(SIGNAL_TYPE_ALL)); + } + // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and // OtlpHttpSpanExporter. private static SpanExporter addAuthorizationHeaders( @@ -97,6 +151,24 @@ private static SpanExporter addAuthorizationHeaders( return exporter; } + // Adds authorization headers to the calls made by the OtlpGrpcMetricExporter and + // OtlpHttpMetricExporter. + private static MetricExporter addAuthorizationHeaders( + MetricExporter exporter, GoogleCredentials credentials) { + if (exporter instanceof OtlpHttpMetricExporter) { + OtlpHttpMetricExporterBuilder builder = + ((OtlpHttpMetricExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); + } else if (exporter instanceof OtlpGrpcMetricExporter) { + OtlpGrpcMetricExporterBuilder builder = + ((OtlpGrpcMetricExporter) exporter) + .toBuilder().setHeaders(() -> getRequiredHeaderMap(credentials)); + return builder.build(); + } + return exporter; + } + private static Map getRequiredHeaderMap(GoogleCredentials credentials) { Map> gcpHeaders; try { 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 5cbee0890..de96dc382 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 @@ -7,6 +7,9 @@ import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY; import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_ALL; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_METRICS; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_TRACES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -17,10 +20,16 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableMap; import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporterBuilder; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; import io.opentelemetry.sdk.OpenTelemetrySdk; @@ -31,8 +40,15 @@ 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.metrics.ConfigurableMetricExporterProvider; import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider; import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.common.export.MemoryMode; +import io.opentelemetry.sdk.metrics.Aggregation; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; @@ -45,10 +61,12 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Random; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.stream.Stream; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -63,19 +81,24 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; @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"; + private static final Random TEST_RANDOM = new Random(); @Mock private GoogleCredentials mockedGoogleCredentials; - @Captor private ArgumentCaptor>> headerSupplierCaptor; + @Captor private ArgumentCaptor>> traceHeaderSupplierCaptor; + @Captor private ArgumentCaptor>> metricHeaderSupplierCaptor; - private static final ImmutableMap otelProperties = + private static final ImmutableMap defaultOtelPropertiesSpanExporter = ImmutableMap.of( + "otel.exporter.otlp.traces.endpoint", + "https://telemetry.googleapis.com/v1/traces", "otel.traces.exporter", "otlp", "otel.metrics.exporter", @@ -85,16 +108,32 @@ class GcpAuthAutoConfigurationCustomizerProviderTest { "otel.resource.attributes", "foo=bar"); + private static final ImmutableMap defaultOtelPropertiesMetricExporter = + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://telemetry.googleapis.com/v1/metrics", + "otel.traces.exporter", + "none", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none", + "otel.resource.attributes", + "foo=bar"); + @BeforeEach public void setup() { MockitoAnnotations.openMocks(this); } + // TODO: Use parameterized test for testing traces customizer for http & grpc. @Test - public void testCustomizerOtlpHttp() { + public void testTraceCustomizerOtlpHttp() { // Set resource project system property System.setProperty( ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); // Prepare mocks prepareMockBehaviorForGoogleCredentials(); OtlpHttpSpanExporter mockOtlpHttpSpanExporter = Mockito.mock(OtlpHttpSpanExporter.class); @@ -113,6 +152,7 @@ public void testCustomizerOtlpHttp() { }); Mockito.when(mockOtlpHttpSpanExporter.toBuilder()).thenReturn(spyOtlpHttpSpanExporterBuilder); + // begin assertions try (MockedStatic googleCredentialsMockedStatic = Mockito.mockStatic(GoogleCredentials.class)) { googleCredentialsMockedStatic @@ -127,9 +167,10 @@ public void testCustomizerOtlpHttp() { Mockito.verify(mockOtlpHttpSpanExporter, Mockito.times(1)).toBuilder(); Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1)) - .setHeaders(headerSupplierCaptor.capture()); - assertEquals(2, headerSupplierCaptor.getValue().get().size()); - assertThat(authHeadersQuotaProjectIsPresent(headerSupplierCaptor.getValue().get())).isTrue(); + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertEquals(2, traceHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); Mockito.verify(mockOtlpHttpSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); @@ -149,19 +190,22 @@ public void testCustomizerOtlpHttp() { } @Test - public void testCustomizerOtlpGrpc() { + public void testTraceCustomizerOtlpGrpc() { // Set resource project system property System.setProperty( ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_TRACES); // Prepare mocks prepareMockBehaviorForGoogleCredentials(); OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = Mockito.spy(OtlpGrpcSpanExporter.builder()); List exportedSpans = new ArrayList<>(); - configureGrpcMockExporters( + configureGrpcMockSpanExporter( mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + // begin assertions try (MockedStatic googleCredentialsMockedStatic = Mockito.mockStatic(GoogleCredentials.class)) { googleCredentialsMockedStatic @@ -176,9 +220,10 @@ public void testCustomizerOtlpGrpc() { Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.times(1)).toBuilder(); Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) - .setHeaders(headerSupplierCaptor.capture()); - assertEquals(2, headerSupplierCaptor.getValue().get().size()); - assertThat(authHeadersQuotaProjectIsPresent(headerSupplierCaptor.getValue().get())).isTrue(); + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertEquals(2, traceHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.atLeast(1)).export(Mockito.anyCollection()); @@ -197,8 +242,133 @@ public void testCustomizerOtlpGrpc() { } } + // TODO: Use parameterized test for testing metrics customizer for http & grpc. + @Test + public void testMetricCustomizerOtlpHttp() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + SIGNAL_TYPE_METRICS); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpHttpMetricExporter mockOtlpHttpMetricExporter = Mockito.mock(OtlpHttpMetricExporter.class); + OtlpHttpMetricExporterBuilder otlpMetricExporterBuilder = OtlpHttpMetricExporter.builder(); + OtlpHttpMetricExporterBuilder spyOtlpHttpMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureHttpMockMetricExporter( + mockOtlpHttpMetricExporter, spyOtlpHttpMetricExporterBuilder, exportedMetrics); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpHttpMetricExporter); + generateTestMetric(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertTrue(joinResult.isSuccess()); + + Mockito.verify(mockOtlpHttpMetricExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpHttpMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertEquals(2, metricHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpHttpMetricExporter, Mockito.atLeast(1)) + .export(Mockito.anyCollection()); + + assertThat(exportedMetrics) + .hasSizeGreaterThan(0) + .allSatisfy( + metricData -> { + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), + DUMMY_GCP_RESOURCE_PROJECT_ID) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(metricData.getLongSumData().getPoints()) + .hasSizeGreaterThan(0) + .allSatisfy( + longPointData -> { + assertThat(longPointData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + }); + }); + } + } + + @Test + public void testMetricCustomizerOtlpGrpc() { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + SIGNAL_TYPE_METRICS); + // Prepare mocks + prepareMockBehaviorForGoogleCredentials(); + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter = Mockito.mock(OtlpGrpcMetricExporter.class); + OtlpGrpcMetricExporterBuilder otlpMetricExporterBuilder = OtlpGrpcMetricExporter.builder(); + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureGrpcMockMetricExporter( + mockOtlpGrpcMetricExporter, spyOtlpGrpcMetricExporterBuilder, exportedMetrics); + + // begin assertions + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = buildOpenTelemetrySdkWithExporter(mockOtlpGrpcMetricExporter); + generateTestMetric(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertTrue(joinResult.isSuccess()); + + Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.times(1)).toBuilder(); + Mockito.verify(spyOtlpGrpcMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertEquals(2, metricHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + + Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.atLeast(1)) + .export(Mockito.anyCollection()); + + assertThat(exportedMetrics) + .hasSizeGreaterThan(0) + .allSatisfy( + metricData -> { + assertThat(metricData.getResource().getAttributes().asMap()) + .containsEntry( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), + DUMMY_GCP_RESOURCE_PROJECT_ID) + .containsEntry(AttributeKey.stringKey("foo"), "bar"); + assertThat(metricData.getLongSumData().getPoints()) + .hasSizeGreaterThan(0) + .allSatisfy( + longPointData -> { + assertThat(longPointData.getAttributes().asMap()) + .containsKey(AttributeKey.longKey("work_loop")); + }); + }); + } + } + @Test public void testCustomizerFailWithMissingResourceProject() { + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_ALL); OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); try (MockedStatic googleCredentialsMockedStatic = Mockito.mockStatic(GoogleCredentials.class)) { @@ -219,6 +389,8 @@ public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws // Set resource project system property System.setProperty( ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), SIGNAL_TYPE_ALL); // Prepare request metadata AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); @@ -252,7 +424,7 @@ public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = Mockito.spy(OtlpGrpcSpanExporter.builder()); List exportedSpans = new ArrayList<>(); - configureGrpcMockExporters( + configureGrpcMockSpanExporter( mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); try (MockedStatic googleCredentialsMockedStatic = @@ -268,10 +440,10 @@ public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); assertTrue(joinResult.isSuccess()); Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) - .setHeaders(headerSupplierCaptor.capture()); + .setHeaders(traceHeaderSupplierCaptor.capture()); // assert that the Authorization bearer token header is present - Map exportHeaders = headerSupplierCaptor.getValue().get(); + Map exportHeaders = traceHeaderSupplierCaptor.getValue().get(); assertThat(exportHeaders).containsEntry("Authorization", "Bearer fake"); if (testCase.getExpectedQuotaProjectInHeader() == null) { @@ -285,6 +457,215 @@ public void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws } } + @ParameterizedTest + @MethodSource("provideTargetSignalBehaviorTestCases") + public void testTargetSignalsBehavior(TargetSignalBehavior testCase) { + // Set resource project system property + System.setProperty( + ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), DUMMY_GCP_RESOURCE_PROJECT_ID); + // Prepare mocks + // Prepare mocked credential + prepareMockBehaviorForGoogleCredentials(); + + // Prepare mocked span exporter + OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = Mockito.mock(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder = + Mockito.spy(OtlpGrpcSpanExporter.builder()); + List exportedSpans = new ArrayList<>(); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + configureGrpcMockSpanExporter( + mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, exportedSpans); + + // Prepare mocked metrics exporter + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter = Mockito.mock(OtlpGrpcMetricExporter.class); + OtlpGrpcMetricExporterBuilder otlpMetricExporterBuilder = OtlpGrpcMetricExporter.builder(); + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder = + Mockito.spy(otlpMetricExporterBuilder); + List exportedMetrics = new ArrayList<>(); + configureGrpcMockMetricExporter( + mockOtlpGrpcMetricExporter, spyOtlpGrpcMetricExporterBuilder, exportedMetrics); + + // configure environment according to test case + System.setProperty( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), + testCase.getConfiguredTargetSignals()); + + // Build Autoconfigured OpenTelemetry SDK using the mocks and send signals + try (MockedStatic googleCredentialsMockedStatic = + Mockito.mockStatic(GoogleCredentials.class)) { + googleCredentialsMockedStatic + .when(GoogleCredentials::getApplicationDefault) + .thenReturn(mockedGoogleCredentials); + + OpenTelemetrySdk sdk = + buildOpenTelemetrySdkWithExporter( + mockOtlpGrpcSpanExporter, + mockOtlpGrpcMetricExporter, + testCase.getUserSpecifiedOtelProperties()); + generateTestMetric(sdk); + generateTestSpan(sdk); + CompletableResultCode code = sdk.shutdown(); + CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS); + assertTrue(joinResult.isSuccess()); + + // Check Traces modification conditions + if (testCase.getExpectedIsTraceSignalModified()) { + // If traces signal is expected to be modified, auth headers must be present + Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1)) + .setHeaders(traceHeaderSupplierCaptor.capture()); + assertEquals(2, traceHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get())) + .isTrue(); + } else { + // If traces signals is not expected to be modified then no interaction with the builder + // should be made + Mockito.verifyNoInteractions(spyOtlpGrpcSpanExporterBuilder); + } + + // Check Metric modification conditions + if (testCase.getExpectedIsMetricsSignalModified()) { + // If metrics signal is expected to be modified, auth headers must be present + Mockito.verify(spyOtlpGrpcMetricExporterBuilder, Mockito.times(1)) + .setHeaders(metricHeaderSupplierCaptor.capture()); + assertEquals(2, metricHeaderSupplierCaptor.getValue().get().size()); + assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get())) + .isTrue(); + } else { + // If metrics signals is not expected to be modified then no interaction with the builder + // should be made + Mockito.verifyNoInteractions(spyOtlpGrpcMetricExporterBuilder); + } + } + } + + /** Test cases specifying expected behavior for GOOGLE_OTEL_AUTH_TARGET_SIGNALS */ + private static Stream provideTargetSignalBehaviorTestCases() { + return Stream.of( + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("traces") + .setUserSpecifiedOtelProperties(defaultOtelPropertiesSpanExporter) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics") + .setUserSpecifiedOtelProperties(defaultOtelPropertiesMetricExporter) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, traces") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(true) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("all") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "none", + "otel.metrics.exporter", + "none", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metric, trace") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(false) + .setExpectedIsTraceSignalModified(false) + .build()), + Arguments.of( + TargetSignalBehavior.builder() + .setConfiguredTargetSignals("metrics, trace") + .setUserSpecifiedOtelProperties( + ImmutableMap.of( + "otel.exporter.otlp.metrics.endpoint", + "https://localhost:4813/v1/metrics", + "otel.exporter.otlp.traces.endpoint", + "https://localhost:4813/v1/traces", + "otel.traces.exporter", + "otlp", + "otel.metrics.exporter", + "otlp", + "otel.logs.exporter", + "none")) + .setExpectedIsMetricsSignalModified(true) + .setExpectedIsTraceSignalModified(false) + .build())); + } + /** * Test cases specifying expected value for the user quota project header given the user input and * the current credentials state. @@ -356,16 +737,20 @@ private static Stream provideQuotaBehaviorTestCases() { .build())); } - // Configure necessary behavior on the Grpc mock exporters to work - // TODO: Potential improvement - make this work for Http exporter as well. - private static void configureGrpcMockExporters( + // Configure necessary behavior on the gRPC mock span exporters to work. + // Mockito.lenient is used here because this method is used with parameterized tests where based + // on certain inputs, certain stubbings may not be required. + private static void configureGrpcMockSpanExporter( OtlpGrpcSpanExporter mockGrpcExporter, OtlpGrpcSpanExporterBuilder spyGrpcExporterBuilder, List exportedSpanContainer) { - Mockito.when(spyGrpcExporterBuilder.build()).thenReturn(mockGrpcExporter); - Mockito.when(mockGrpcExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess()); - Mockito.when(mockGrpcExporter.toBuilder()).thenReturn(spyGrpcExporterBuilder); - Mockito.when(mockGrpcExporter.export(Mockito.anyCollection())) + Mockito.lenient().when(spyGrpcExporterBuilder.build()).thenReturn(mockGrpcExporter); + Mockito.lenient() + .when(mockGrpcExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.lenient().when(mockGrpcExporter.toBuilder()).thenReturn(spyGrpcExporterBuilder); + Mockito.lenient() + .when(mockGrpcExporter.export(Mockito.anyCollection())) .thenAnswer( invocationOnMock -> { exportedSpanContainer.addAll(invocationOnMock.getArgument(0)); @@ -373,6 +758,88 @@ private static void configureGrpcMockExporters( }); } + // Configure necessary behavior on the http mock metric exporters to work. + private static void configureHttpMockMetricExporter( + OtlpHttpMetricExporter mockOtlpHttpMetricExporter, + OtlpHttpMetricExporterBuilder spyOtlpHttpMetricExporterBuilder, + List exportedMetricContainer) { + Mockito.when(spyOtlpHttpMetricExporterBuilder.build()).thenReturn(mockOtlpHttpMetricExporter); + Mockito.when(mockOtlpHttpMetricExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.when(mockOtlpHttpMetricExporter.toBuilder()) + .thenReturn(spyOtlpHttpMetricExporterBuilder); + Mockito.when(mockOtlpHttpMetricExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedMetricContainer.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + // mock the get default aggregation and aggregation temporality - they're required for valid + // metric collection. + Mockito.when(mockOtlpHttpMetricExporter.getDefaultAggregation(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpHttpMetricExporter.getDefault().getDefaultAggregation(instrumentType); + }); + Mockito.when(mockOtlpHttpMetricExporter.getAggregationTemporality(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpHttpMetricExporter.getDefault() + .getAggregationTemporality(instrumentType); + }); + } + + // Configure necessary behavior on the gRPC mock metrics exporters to work. + // Mockito.lenient is used here because this method is used with parameterized tests where based + // on certain inputs, certain stubbings may not be required. + private static void configureGrpcMockMetricExporter( + OtlpGrpcMetricExporter mockOtlpGrpcMetricExporter, + OtlpGrpcMetricExporterBuilder spyOtlpGrpcMetricExporterBuilder, + List exportedMetricContainer) { + Mockito.lenient() + .when(spyOtlpGrpcMetricExporterBuilder.build()) + .thenReturn(mockOtlpGrpcMetricExporter); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.shutdown()) + .thenReturn(CompletableResultCode.ofSuccess()); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.toBuilder()) + .thenReturn(spyOtlpGrpcMetricExporterBuilder); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.export(Mockito.anyCollection())) + .thenAnswer( + invocationOnMock -> { + exportedMetricContainer.addAll(invocationOnMock.getArgument(0)); + return CompletableResultCode.ofSuccess(); + }); + // mock the get default aggregation and aggregation temporality - they're required for valid + // metric collection. + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getDefaultAggregation(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpGrpcMetricExporter.getDefault().getDefaultAggregation(instrumentType); + }); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getAggregationTemporality(Mockito.any())) + .thenAnswer( + (Answer) + invocationOnMock -> { + InstrumentType instrumentType = invocationOnMock.getArgument(0); + return OtlpGrpcMetricExporter.getDefault() + .getAggregationTemporality(instrumentType); + }); + Mockito.lenient() + .when(mockOtlpGrpcMetricExporter.getMemoryMode()) + .thenReturn(MemoryMode.IMMUTABLE_DATA); + } + @AutoValue abstract static class QuotaProjectIdTestBehavior { // A null user specified quota represents the use case where user omits specifying quota @@ -409,11 +876,48 @@ abstract static class Builder { } } + @AutoValue + abstract static class TargetSignalBehavior { + @Nonnull + abstract String getConfiguredTargetSignals(); + + @Nonnull + abstract ImmutableMap getUserSpecifiedOtelProperties(); + + abstract boolean getExpectedIsTraceSignalModified(); + + abstract boolean getExpectedIsMetricsSignalModified(); + + static Builder builder() { + return new AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_TargetSignalBehavior + .Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setConfiguredTargetSignals(String targetSignals); + + abstract Builder setUserSpecifiedOtelProperties(Map oTelProperties); + + // Set whether the combination of specified OTel properties and configured target signals + // should lead to modification of the OTLP trace exporters. + abstract Builder setExpectedIsTraceSignalModified(boolean expectedModified); + + // Set whether the combination of specified OTel properties and configured target signals + // should lead to modification of the OTLP metrics exporters. + abstract Builder setExpectedIsMetricsSignalModified(boolean expectedModified); + + abstract TargetSignalBehavior build(); + } + } + + // Mockito.lenient is used here because this method is used with parameterized tests where based @SuppressWarnings("CannotMockMethod") private void prepareMockBehaviorForGoogleCredentials() { AccessToken fakeAccessToken = new AccessToken("fake", Date.from(Instant.now())); try { - Mockito.when(mockedGoogleCredentials.getRequestMetadata()) + Mockito.lenient() + .when(mockedGoogleCredentials.getRequestMetadata()) .thenReturn( ImmutableMap.of( "Authorization", @@ -426,10 +930,38 @@ private void prepareMockBehaviorForGoogleCredentials() { } private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter spanExporter) { + return buildOpenTelemetrySdkWithExporter( + spanExporter, OtlpHttpMetricExporter.getDefault(), defaultOtelPropertiesSpanExporter); + } + + @SuppressWarnings("UnusedMethod") + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + SpanExporter spanExporter, ImmutableMap customOTelProperties) { + return buildOpenTelemetrySdkWithExporter( + spanExporter, OtlpHttpMetricExporter.getDefault(), customOTelProperties); + } + + @SuppressWarnings("UnusedMethod") + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(MetricExporter metricExporter) { + return buildOpenTelemetrySdkWithExporter( + OtlpHttpSpanExporter.getDefault(), metricExporter, defaultOtelPropertiesMetricExporter); + } + + @SuppressWarnings("UnusedMethod") + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + MetricExporter metricExporter, ImmutableMap customOtelProperties) { + return buildOpenTelemetrySdkWithExporter( + OtlpHttpSpanExporter.getDefault(), metricExporter, customOtelProperties); + } + + private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter( + SpanExporter spanExporter, + MetricExporter metricExporter, + ImmutableMap customOtelProperties) { SpiHelper spiHelper = SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader()); AutoConfiguredOpenTelemetrySdkBuilder builder = - AutoConfiguredOpenTelemetrySdk.builder().addPropertiesSupplier(() -> otelProperties); + AutoConfiguredOpenTelemetrySdk.builder().addPropertiesSupplier(() -> customOtelProperties); AutoConfigureUtil.setComponentLoader( builder, new ComponentLoader() { @@ -451,6 +983,21 @@ public String getName() { } }); } + if (spiClass == ConfigurableMetricExporterProvider.class) { + return Collections.singletonList( + (T) + new ConfigurableMetricExporterProvider() { + @Override + public MetricExporter createExporter(ConfigProperties configProperties) { + return metricExporter; + } + + @Override + public String getName() { + return "otlp"; + } + }); + } return spiHelper.load(spiClass); } }); @@ -476,6 +1023,19 @@ private static void generateTestSpan(OpenTelemetrySdk openTelemetrySdk) { } } + private static void generateTestMetric(OpenTelemetrySdk openTelemetrySdk) { + LongCounter longCounter = + openTelemetrySdk + .getMeter("test") + .counterBuilder("sample") + .setDescription("sample counter") + .setUnit("1") + .build(); + long workOutput = busyloop(); + long randomValue = TEST_RANDOM.nextInt(1000); + longCounter.add(randomValue, Attributes.of(AttributeKey.longKey("work_loop"), workOutput)); + } + // loop to simulate work done private static long busyloop() { Instant start = Instant.now();