This is an automated email from the ASF dual-hosted git repository.

stankiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 4fe3342c71f [OpenTelemetry] Add gcp otel auth extension (#38773)
4fe3342c71f is described below

commit 4fe3342c71f621c13e5f91ad930f5b10e183711e
Author: RadosÅ‚aw Stankiewicz <[email protected]>
AuthorDate: Fri Jun 5 17:14:56 2026 +0200

    [OpenTelemetry] Add gcp otel auth extension (#38773)
    
    * migrate otel auth extension.
---
 .../org/apache/beam/gradle/BeamModulePlugin.groovy |    2 +
 .../opentelemetry-gcp-auth-extension/build.gradle  |   50 +
 .../opentelemetry/gcp/auth/ConfigurableOption.java |  163 +++
 ...GcpAuthAutoConfigurationCustomizerProvider.java |  282 +++++
 .../gcp/auth/GoogleAuthException.java              |   69 ++
 .../opentelemetry/gcp/auth/package-info.java       |   20 +
 ...nfigure.spi.AutoConfigurationCustomizerProvider |   16 +
 ...uthAutoConfigurationCustomizerProviderTest.java | 1234 ++++++++++++++++++++
 settings.gradle.kts                                |    1 +
 9 files changed, 1837 insertions(+)

diff --git 
a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy 
b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
index ed2f9163432..9f34fb54e47 100644
--- a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
+++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamModulePlugin.groovy
@@ -869,6 +869,8 @@ class BeamModulePlugin implements Plugin<Project> {
         opentelemetry_sdk                           : 
"io.opentelemetry:opentelemetry-sdk", // opentelemetry-bom sets version
         opentelemetry_exporter_otlp                 : 
"io.opentelemetry:opentelemetry-exporter-otlp", // opentelemetry-bom sets 
version
         opentelemetry_extension_autoconfigure       : 
"io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", // 
opentelemetry-bom sets version
+        opentelemetry_proto                         : 
"io.opentelemetry.proto:opentelemetry-proto:$opentelemetry_version-alpha",
+        opentelemetry_sdk_testing                   : 
"io.opentelemetry:opentelemetry-sdk-testing:$opentelemetry_version",
         postgres                                    : 
"org.postgresql:postgresql:$postgres_version",
         protobuf_java                               : 
"com.google.protobuf:protobuf-java:$protobuf_version",
         protobuf_java_util                          : 
"com.google.protobuf:protobuf-java-util:$protobuf_version",
diff --git a/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle
new file mode 100644
index 00000000000..1b08b867b29
--- /dev/null
+++ b/sdks/java/extensions/opentelemetry-gcp-auth-extension/build.gradle
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins { id 'org.apache.beam.module' }
+applyJavaNature(
+  automaticModuleName: 'org.apache.beam.sdk.extensions.opentelemetry.gcp.auth',
+)
+
+description = "Apache Beam :: SDKs :: Java :: Extensions :: OpenTelemetry GCP 
Auth"
+ext.summary = "OpenTelemetry extension that provides GCP authentication 
support for OTLP exporters."
+
+dependencies {
+  implementation 
enforcedPlatform(library.java.google_cloud_platform_libraries_bom)
+  implementation platform(library.java.opentelemetry_bom)
+  implementation library.java.google_auth_library_oauth2_http
+  implementation library.java.vendored_guava_32_1_2_jre
+  compileOnly library.java.opentelemetry_api
+  compileOnly library.java.opentelemetry_extension_autoconfigure
+  compileOnly library.java.opentelemetry_exporter_otlp
+  implementation project(path: ":sdks:java:core", configuration: "shadow")
+
+  testImplementation library.java.junit
+  testImplementation library.java.mockito_core
+  testImplementation library.java.mockito_inline
+  testImplementation library.java.jupiter_api
+  testImplementation library.java.jupiter_params
+  testRuntimeOnly library.java.jupiter_engine
+  testImplementation library.java.truth
+  testImplementation library.java.opentelemetry_api
+  testImplementation library.java.opentelemetry_sdk
+  testImplementation library.java.opentelemetry_exporter_otlp
+  testImplementation library.java.opentelemetry_sdk_testing
+  testImplementation library.java.opentelemetry_extension_autoconfigure
+}
+test { useJUnitPlatform() }
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java
new file mode 100644
index 00000000000..e18e5693e3a
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/ConfigurableOption.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth;
+
+import static java.util.Locale.ROOT;
+
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
+import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * An enum representing configurable options for a GCP Authentication 
Extension. Each option has a
+ * user-readable name and can be configured using environment variables or 
system properties.
+ *
+ * <p>Copied from
+ * 
https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java
+ */
+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"),
+
+  /**
+   * 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"),
+
+  /**
+   * 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.
+   *
+   * <p>Valid signal values are:
+   *
+   * <ul>
+   *   <li>{@code metrics} - Enables authentication for metric exports.
+   *   <li>{@code traces} - Enables authentication for trace exports.
+   *   <li>{@code all} - Enables authentication for all exports.
+   * </ul>
+   *
+   * <p>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 Authentication 
Extension");
+
+  private final String userReadableName;
+  private final String environmentVariableName;
+  private final String systemPropertyName;
+
+  ConfigurableOption(String userReadableName) {
+    this.userReadableName = userReadableName;
+    this.environmentVariableName = this.name();
+    this.systemPropertyName = 
this.environmentVariableName.toLowerCase(ROOT).replace('_', '.');
+  }
+
+  /**
+   * Returns the environment variable name associated with this option.
+   *
+   * @return the environment variable name (e.g., GOOGLE_CLOUD_PROJECT)
+   */
+  String getEnvironmentVariable() {
+    return this.environmentVariableName;
+  }
+
+  /**
+   * Returns the system property name associated with this option.
+   *
+   * @return the system property name (e.g., google.cloud.project)
+   */
+  String getSystemProperty() {
+    return this.systemPropertyName;
+  }
+
+  /**
+   * Returns the user readable name associated with this option.
+   *
+   * @return the user readable name (e.g., "Google Cloud Quota Project ID")
+   */
+  String getUserReadableName() {
+    return this.userReadableName;
+  }
+
+  /**
+   * Retrieves the configured value for this option. This method checks the 
environment variable
+   * first and then the system property.
+   *
+   * @return The configured value as a string, or throws an exception if not 
configured.
+   * @throws ConfigurationException if neither the environment variable nor 
the system property is
+   *     set.
+   */
+  String getConfiguredValue(ConfigProperties configProperties) {
+    String configuredValue = 
configProperties.getString(this.getSystemProperty());
+    if (configuredValue != null && !configuredValue.isEmpty()) {
+      return configuredValue;
+    } else {
+      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",
+              this.userReadableName, this.getEnvironmentVariable(), 
this.getSystemProperty()));
+    }
+  }
+
+  /**
+   * Retrieves the value for this option, prioritizing environment variables 
and system properties.
+   * If neither an environment variable nor a system property is set for this 
option, the provided
+   * fallback function is used to determine the value.
+   *
+   * @param fallback A {@link Supplier} that provides the default value for 
the option when it is
+   *     not explicitly configured via an environment variable or system 
property.
+   * @return The configured value for the option, obtained from the 
environment variable, system
+   *     property, or the fallback function, in that order of precedence.
+   */
+  String getConfiguredValueWithFallback(
+      ConfigProperties configProperties, Supplier<String> fallback) {
+    try {
+      return this.getConfiguredValue(configProperties);
+    } catch (ConfigurationException e) {
+      return fallback.get();
+    }
+  }
+
+  /**
+   * Retrieves the value for this option, prioritizing environment variables 
before system
+   * properties. If neither an environment variable nor a system property is 
set for this option,
+   * then an empty {@link Optional} is returned.
+   *
+   * @return The configured value for the option, if set, obtained from the 
environment variable,
+   *     system property, or empty {@link Optional}, in that order of 
precedence.
+   */
+  Optional<String> getConfiguredValueAsOptional(ConfigProperties 
configProperties) {
+    try {
+      return Optional.of(this.getConfiguredValue(configProperties));
+    } catch (ConfigurationException e) {
+      return Optional.empty();
+    }
+  }
+}
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java
new file mode 100644
index 00000000000..ce2b142591b
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth;
+
+import static io.opentelemetry.api.common.AttributeKey.stringKey;
+import static java.util.Arrays.stream;
+import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toMap;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auto.service.AutoService;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter;
+import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
+import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter;
+import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
+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;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import org.apache.beam.sdk.annotations.Internal;
+import 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GoogleAuthException.Reason;
+
+/**
+ * An AutoConfigurationCustomizerProvider for Google Cloud Platform (GCP) 
OpenTelemetry (OTLP)
+ * integration.
+ *
+ * <p>This class is registered as a service provider using {@link AutoService} 
and is responsible
+ * for customizing the OpenTelemetry configuration for GCP specific behavior. 
It retrieves Google
+ * Application Default Credentials (ADC) and adds them as authorization 
headers to the configured
+ * {@link SpanExporter}. It also sets default properties and resource 
attributes for GCP
+ * integration.
+ *
+ * <p>Copied from
+ * 
https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java
+ *
+ * @see AutoConfigurationCustomizerProvider
+ * @see GoogleCredentials
+ */
+@AutoService(AutoConfigurationCustomizerProvider.class)
+@Internal
+public class GcpAuthAutoConfigurationCustomizerProvider
+    implements AutoConfigurationCustomizerProvider {
+
+  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";
+
+  private @Nullable GoogleCredentials credentials;
+
+  /**
+   * Customizes the provided {@link AutoConfigurationCustomizer} such that 
authenticated exports to
+   * GCP Telemetry API are possible from the configured OTLP exporter.
+   *
+   * <p>This method performs the following:
+   *
+   * <ul>
+   *   <li>Verifies whether the configured OTLP endpoint (base or signal 
specific) is a known GCP
+   *       endpoint.
+   *   <li>If the configured base OTLP endpoint is a known GCP Telemetry API 
endpoint, customizes
+   *       both the configured OTLP {@link SpanExporter} and {@link 
MetricExporter}.
+   *   <li>If the configured signal specific endpoint is a known GCP Telemetry 
API endpoint,
+   *       customizes only the signal specific exporter.
+   * </ul>
+   *
+   * 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.
+   */
+  @Override
+  public void customize(@Nonnull AutoConfigurationCustomizer 
autoConfiguration) {
+    autoConfiguration
+        .addSpanExporterCustomizer(this::customizeSpanExporter)
+        .addMetricExporterCustomizer(this::customizeMetricExporter)
+        .addResourceCustomizer(this::customizeResource);
+  }
+
+  @Override
+  public int order() {
+    return Integer.MAX_VALUE - 1;
+  }
+
+  private synchronized GoogleCredentials getCredentials() {
+    if (credentials == null) {
+      try {
+        credentials = GoogleCredentials.getApplicationDefault();
+      } catch (IOException e) {
+        throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e);
+      }
+    }
+    return credentials;
+  }
+
+  private SpanExporter customizeSpanExporter(
+      SpanExporter exporter, ConfigProperties configProperties) {
+    if (isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)) {
+      return addAuthorizationHeaders(exporter, configProperties);
+    }
+    return exporter;
+  }
+
+  private MetricExporter customizeMetricExporter(
+      MetricExporter exporter, ConfigProperties configProperties) {
+    if (isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties)) {
+      return addAuthorizationHeaders(exporter, configProperties);
+    }
+    return exporter;
+  }
+
+  // Checks if the auth extension is configured to target the passed signal 
for authentication.
+  private static boolean isSignalTargeted(String checkSignal, ConfigProperties 
configProperties) {
+    String endpoint = configProperties.getString("otel.exporter.otlp." + 
checkSignal + ".endpoint");
+    if (endpoint == null) {
+      endpoint = configProperties.getString("otel.exporter.otlp.endpoint");
+    }
+    if (endpoint == null) {
+      return false;
+    }
+
+    try {
+      java.net.URI uri = new java.net.URI(endpoint);
+      String host = uri.getHost();
+      String scheme = uri.getScheme();
+      if (host == null
+          || scheme == null
+          || !scheme.equalsIgnoreCase("https")
+          || (!host.equalsIgnoreCase("telemetry.googleapis.com")
+              && !host.equalsIgnoreCase("telemetry.mtls.googleapis.com"))) {
+        return false;
+      }
+    } catch (java.net.URISyntaxException e) {
+      return false;
+    }
+
+    String userSpecifiedTargetedSignals =
+        
ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback(
+            configProperties, () -> SIGNAL_TYPE_ALL);
+    return stream(userSpecifiedTargetedSignals.split(","))
+        .map(String::trim)
+        .anyMatch(
+            targetedSignal ->
+                targetedSignal.equals(checkSignal) || 
targetedSignal.equals(SIGNAL_TYPE_ALL));
+  }
+
+  private boolean isAnySignalTargeted(ConfigProperties configProperties) {
+    return isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)
+        || isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties);
+  }
+
+  // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter 
and
+  // OtlpHttpSpanExporter.
+  private SpanExporter addAuthorizationHeaders(
+      SpanExporter exporter, ConfigProperties configProperties) {
+    if (exporter instanceof OtlpHttpSpanExporter) {
+      SpanExporter result =
+          ((OtlpHttpSpanExporter) exporter)
+              .toBuilder()
+              .setHeaders(() -> getRequiredHeaderMap(configProperties))
+              .build();
+      exporter.shutdown();
+      return result;
+    } else if (exporter instanceof OtlpGrpcSpanExporter) {
+      SpanExporter result =
+          ((OtlpGrpcSpanExporter) exporter)
+              .toBuilder()
+              .setHeaders(() -> getRequiredHeaderMap(configProperties))
+              .build();
+      exporter.shutdown();
+      return result;
+    }
+    return exporter;
+  }
+
+  // Adds authorization headers to the calls made by the 
OtlpGrpcMetricExporter and
+  // OtlpHttpMetricExporter.
+  private MetricExporter addAuthorizationHeaders(
+      MetricExporter exporter, ConfigProperties configProperties) {
+    if (exporter instanceof OtlpHttpMetricExporter) {
+      MetricExporter result =
+          ((OtlpHttpMetricExporter) exporter)
+              .toBuilder()
+              .setHeaders(() -> getRequiredHeaderMap(configProperties))
+              .build();
+      exporter.shutdown();
+      return result;
+    } else if (exporter instanceof OtlpGrpcMetricExporter) {
+      MetricExporter result =
+          ((OtlpGrpcMetricExporter) exporter)
+              .toBuilder()
+              .setHeaders(() -> getRequiredHeaderMap(configProperties))
+              .build();
+      exporter.shutdown();
+      return result;
+    }
+    return exporter;
+  }
+
+  private Map<String, String> getRequiredHeaderMap(ConfigProperties 
configProperties) {
+    GoogleCredentials creds = getCredentials();
+    Map<String, List<String>> gcpHeaders;
+    try {
+      // this also refreshes the credentials, if required
+      gcpHeaders = creds.getRequestMetadata();
+    } catch (IOException e) {
+      throw new GoogleAuthException(Reason.FAILED_ADC_REFRESH, e);
+    }
+    if (gcpHeaders == null) {
+      return Map.of();
+    }
+    Map<String, String> flattenedHeaders =
+        gcpHeaders.entrySet().stream()
+            .filter(entry -> entry.getKey() != null && entry.getValue() != 
null)
+            .collect(
+                toMap(
+                    Map.Entry::getKey,
+                    entry ->
+                        entry.getValue().stream()
+                            .filter(Objects::nonNull) // Filter nulls
+                            .filter(s -> !s.isEmpty()) // Filter empty strings
+                            .collect(joining(","))));
+    // Add quota user project header if not detected by the auth library and 
user provided it via
+    // system properties.
+    if (!flattenedHeaders.containsKey(QUOTA_USER_PROJECT_HEADER)) {
+      Optional<String> maybeConfiguredQuotaProjectId =
+          
ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueAsOptional(
+              configProperties);
+      maybeConfiguredQuotaProjectId.ifPresent(
+          configuredQuotaProjectId ->
+              flattenedHeaders.put(QUOTA_USER_PROJECT_HEADER, 
configuredQuotaProjectId));
+    }
+    return flattenedHeaders;
+  }
+
+  // Updates the current resource with the attributes required for ingesting 
OTLP data on GCP.
+  private Resource customizeResource(Resource resource, ConfigProperties 
configProperties) {
+    if (!isAnySignalTargeted(configProperties)) {
+      return resource;
+    }
+
+    String gcpProjectId;
+    try {
+      gcpProjectId = 
ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties);
+    } catch (ConfigurationException e) {
+      gcpProjectId = getCredentials().getProjectId();
+      if (gcpProjectId == null || gcpProjectId.isEmpty()) {
+        throw e;
+      }
+    }
+    Resource res = 
Resource.create(Attributes.of(stringKey(GCP_USER_PROJECT_ID_KEY), 
gcpProjectId));
+    return resource.merge(res);
+  }
+}
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java
new file mode 100644
index 00000000000..978b4893406
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GoogleAuthException.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth;
+
+/**
+ * An unchecked exception indicating a failure during Google authentication. 
This exception is
+ * thrown when there are issues with retrieving or refreshing Google 
Application Default Credentials
+ * (ADC).
+ *
+ * <p>Copied from
+ * 
https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GoogleAuthException.java
+ */
+public class GoogleAuthException extends RuntimeException {
+
+  private static final long serialVersionUID = 149908685226796448L;
+
+  /**
+   * Constructs a new {@code GoogleAuthException} with the specified reason 
and cause.
+   *
+   * @param reason the reason for the authentication failure.
+   * @param cause the underlying cause of the exception (e.g., an IOException).
+   */
+  GoogleAuthException(Reason reason, Throwable cause) {
+    super(reason.message, cause);
+  }
+
+  /** Enumerates the possible reasons for a Google authentication failure. */
+  enum Reason {
+    /** Indicates a failure to retrieve Google Application Default 
Credentials. */
+    FAILED_ADC_RETRIEVAL("Unable to retrieve Google Application Default 
Credentials."),
+    /** Indicates a failure to retrieve Google Application Default 
Credentials. */
+    FAILED_ADC_REFRESH("Unable to refresh Google Application Default 
Credentials.");
+
+    private final String message;
+
+    /**
+     * Constructs a new {@code Reason} with the specified message.
+     *
+     * @param message the message describing the reason.
+     */
+    Reason(String message) {
+      this.message = message;
+    }
+
+    /**
+     * Returns the message associated with this reason.
+     *
+     * @return the message describing the reason.
+     */
+    public String getMessage() {
+      return message;
+    }
+  }
+}
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java
new file mode 100644
index 00000000000..e389a1a7dc9
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Google Cloud Platform (GCP) OpenTelemetry (OTLP) authentication extension. 
*/
+package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth;
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
new file mode 100644
index 00000000000..bf1ba2cad98
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider
diff --git 
a/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java
 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java
new file mode 100644
index 00000000000..e1db55c3635
--- /dev/null
+++ 
b/sdks/java/extensions/opentelemetry-gcp-auth-extension/src/test/java/org/apache/beam/sdk/extensions/opentelemetry/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java
@@ -0,0 +1,1234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.sdk.extensions.opentelemetry.gcp.auth;
+
+import static com.google.common.truth.Truth.assertThat;
+import static 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.GCP_USER_PROJECT_ID_KEY;
+import static 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.QUOTA_USER_PROJECT_HEADER;
+import static 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_ALL;
+import static 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_METRICS;
+import static 
org.apache.beam.sdk.extensions.opentelemetry.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_TRACES;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auto.value.AutoValue;
+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.common.ComponentLoader;
+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;
+import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
+import 
io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder;
+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;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.AbstractMap.SimpleEntry;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+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.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+/**
+ * Copied from open-telemetry. Link:
+ * 
https://github.com/open-telemetry/opentelemetry-java-contrib/blob/main/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProviderTest.java
+ */
+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<Supplier<Map<String, String>>> 
traceHeaderSupplierCaptor;
+  @Captor private ArgumentCaptor<Supplier<Map<String, String>>> 
metricHeaderSupplierCaptor;
+
+  private static final ImmutableMap<String, String> 
DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER =
+      ImmutableMap.<String, String>builder()
+          .put("otel.exporter.otlp.traces.endpoint", 
"https://telemetry.googleapis.com/v1/traces";)
+          .put("otel.traces.exporter", "otlp")
+          .put("otel.metrics.exporter", "none")
+          .put("otel.logs.exporter", "none")
+          .put("otel.resource.attributes", "foo=bar")
+          .build();
+
+  private static final ImmutableMap<String, String> 
DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER =
+      ImmutableMap.<String, String>builder()
+          .put("otel.exporter.otlp.metrics.endpoint", 
"https://telemetry.googleapis.com/v1/metrics";)
+          .put("otel.traces.exporter", "none")
+          .put("otel.metrics.exporter", "otlp")
+          .put("otel.logs.exporter", "none")
+          .put("otel.resource.attributes", "foo=bar")
+          .build();
+
+  @BeforeEach
+  public void setup() {
+    MockitoAnnotations.openMocks(this);
+  }
+
+  @AfterEach
+  public void teardown() {
+    
System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty());
+    
System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty());
+    
System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty());
+  }
+
+  // TODO: Use parameterized test for testing traces customizer for http & 
grpc.
+  @Test
+  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 = 
mock(OtlpHttpSpanExporter.class);
+    OtlpHttpSpanExporterBuilder otlpSpanExporterBuilder = 
OtlpHttpSpanExporter.builder();
+    OtlpHttpSpanExporterBuilder spyOtlpHttpSpanExporterBuilder =
+        Mockito.spy(otlpSpanExporterBuilder);
+    
when(spyOtlpHttpSpanExporterBuilder.build()).thenReturn(mockOtlpHttpSpanExporter);
+
+    
when(mockOtlpHttpSpanExporter.shutdown()).thenReturn(CompletableResultCode.ofSuccess());
+    List<SpanData> exportedSpans = new ArrayList<>();
+    when(mockOtlpHttpSpanExporter.export(any()))
+        .thenAnswer(
+            invocationOnMock -> {
+              exportedSpans.addAll(invocationOnMock.getArgument(0));
+              return CompletableResultCode.ofSuccess();
+            });
+    
Mockito.when(mockOtlpHttpSpanExporter.toBuilder()).thenReturn(spyOtlpHttpSpanExporterBuilder);
+
+    // begin assertions
+    try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
+        Mockito.mockStatic(GoogleCredentials.class)) {
+      googleCredentialsMockedStatic
+          .when(GoogleCredentials::getApplicationDefault)
+          .thenReturn(mockedGoogleCredentials);
+
+      OpenTelemetrySdk sdk = 
buildOpenTelemetrySdkWithExporter(mockOtlpHttpSpanExporter);
+      generateTestSpan(sdk);
+      CompletableResultCode code = sdk.shutdown();
+      CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
+      assertThat(joinResult.isSuccess()).isTrue();
+
+      Mockito.verify(mockOtlpHttpSpanExporter, Mockito.times(1)).toBuilder();
+      Mockito.verify(spyOtlpHttpSpanExporterBuilder, Mockito.times(1))
+          .setHeaders(traceHeaderSupplierCaptor.capture());
+      
assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+      
assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get()))
+          .isTrue();
+
+      Mockito.verify(mockOtlpHttpSpanExporter, 
Mockito.atLeast(1)).export(Mockito.anyCollection());
+
+      assertThat(exportedSpans).isNotEmpty();
+      for (SpanData spanData : exportedSpans) {
+        assertThat(spanData.getResource().getAttributes().asMap())
+            .containsEntry(
+                AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), 
DUMMY_GCP_RESOURCE_PROJECT_ID);
+        assertThat(spanData.getResource().getAttributes().asMap())
+            .containsEntry(AttributeKey.stringKey("foo"), "bar");
+        
assertThat(spanData.getAttributes().asMap()).containsKey(AttributeKey.longKey("work_loop"));
+      }
+    }
+  }
+
+  @Test
+  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<SpanData> exportedSpans = new ArrayList<>();
+    configureGrpcMockSpanExporter(
+        mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, 
exportedSpans);
+
+    // begin assertions
+    try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
+        Mockito.mockStatic(GoogleCredentials.class)) {
+      googleCredentialsMockedStatic
+          .when(GoogleCredentials::getApplicationDefault)
+          .thenReturn(mockedGoogleCredentials);
+
+      OpenTelemetrySdk sdk = 
buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
+      generateTestSpan(sdk);
+      CompletableResultCode code = sdk.shutdown();
+      CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
+      assertThat(joinResult.isSuccess()).isTrue();
+
+      Mockito.verify(mockOtlpGrpcSpanExporter, Mockito.times(1)).toBuilder();
+      Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1))
+          .setHeaders(traceHeaderSupplierCaptor.capture());
+      
assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+      
assertThat(authHeadersQuotaProjectIsPresent(traceHeaderSupplierCaptor.getValue().get()))
+          .isTrue();
+
+      Mockito.verify(mockOtlpGrpcSpanExporter, 
Mockito.atLeast(1)).export(Mockito.anyCollection());
+
+      assertThat(exportedSpans).isNotEmpty();
+      for (SpanData spanData : exportedSpans) {
+        assertThat(spanData.getResource().getAttributes().asMap())
+            .containsEntry(
+                AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), 
DUMMY_GCP_RESOURCE_PROJECT_ID);
+        assertThat(spanData.getResource().getAttributes().asMap())
+            .containsEntry(AttributeKey.stringKey("foo"), "bar");
+        
assertThat(spanData.getAttributes().asMap()).containsKey(AttributeKey.longKey("work_loop"));
+      }
+    }
+  }
+
+  @Test
+  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<MetricData> exportedMetrics = new ArrayList<>();
+    configureHttpMockMetricExporter(
+        mockOtlpHttpMetricExporter, spyOtlpHttpMetricExporterBuilder, 
exportedMetrics);
+
+    // begin assertions
+    try (MockedStatic<GoogleCredentials> 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);
+      assertThat(joinResult.isSuccess()).isTrue();
+
+      Mockito.verify(mockOtlpHttpMetricExporter, Mockito.times(1)).toBuilder();
+      Mockito.verify(spyOtlpHttpMetricExporterBuilder, Mockito.times(1))
+          .setHeaders(metricHeaderSupplierCaptor.capture());
+      
assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+      
assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get()))
+          .isTrue();
+
+      Mockito.verify(mockOtlpHttpMetricExporter, Mockito.atLeast(1))
+          .export(Mockito.anyCollection());
+
+      assertThat(exportedMetrics).isNotEmpty();
+      for (MetricData metricData : exportedMetrics) {
+        assertThat(metricData.getResource().getAttributes().asMap())
+            .containsEntry(
+                AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), 
DUMMY_GCP_RESOURCE_PROJECT_ID);
+        assertThat(metricData.getResource().getAttributes().asMap())
+            .containsEntry(AttributeKey.stringKey("foo"), "bar");
+        assertThat(metricData.getLongSumData().getPoints()).isNotEmpty();
+        for (io.opentelemetry.sdk.metrics.data.PointData pointData :
+            metricData.getLongSumData().getPoints()) {
+          assertThat(pointData.getAttributes().asMap())
+              .containsKey(AttributeKey.longKey("work_loop"));
+        }
+      }
+    }
+  }
+
+  @Test
+  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<MetricData> exportedMetrics = new ArrayList<>();
+    configureGrpcMockMetricExporter(
+        mockOtlpGrpcMetricExporter, spyOtlpGrpcMetricExporterBuilder, 
exportedMetrics);
+
+    // begin assertions
+    try (MockedStatic<GoogleCredentials> 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);
+      assertThat(joinResult.isSuccess()).isTrue();
+
+      Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.times(1)).toBuilder();
+      Mockito.verify(spyOtlpGrpcMetricExporterBuilder, Mockito.times(1))
+          .setHeaders(metricHeaderSupplierCaptor.capture());
+      
assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+      
assertThat(authHeadersQuotaProjectIsPresent(metricHeaderSupplierCaptor.getValue().get()))
+          .isTrue();
+
+      Mockito.verify(mockOtlpGrpcMetricExporter, Mockito.atLeast(1))
+          .export(Mockito.anyCollection());
+
+      assertThat(exportedMetrics).isNotEmpty();
+      for (MetricData metricData : exportedMetrics) {
+        assertThat(metricData.getResource().getAttributes().asMap())
+            .containsEntry(
+                AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), 
DUMMY_GCP_RESOURCE_PROJECT_ID);
+        assertThat(metricData.getResource().getAttributes().asMap())
+            .containsEntry(AttributeKey.stringKey("foo"), "bar");
+        assertThat(metricData.getLongSumData().getPoints()).isNotEmpty();
+        for (io.opentelemetry.sdk.metrics.data.PointData pointData :
+            metricData.getLongSumData().getPoints()) {
+          assertThat(pointData.getAttributes().asMap())
+              .containsKey(AttributeKey.longKey("work_loop"));
+        }
+      }
+    }
+  }
+
+  @Test
+  void testCustomizerFailWithMissingResourceProject() {
+    System.setProperty(
+        
ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), 
SIGNAL_TYPE_ALL);
+    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));
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("provideQuotaBehaviorTestCases")
+  @SuppressWarnings("CannotMockMethod")
+  void testQuotaProjectBehavior(QuotaProjectIdTestBehavior testCase) throws 
IOException {
+    // 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()));
+    ImmutableMap<String, List<String>> mockedRequestMetadata;
+    if (testCase.getIsQuotaProjectPresentInMetadata()) {
+      mockedRequestMetadata =
+          ImmutableMap.of(
+              "Authorization",
+              Collections.singletonList("Bearer " + 
fakeAccessToken.getTokenValue()),
+              QUOTA_USER_PROJECT_HEADER,
+              Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID));
+    } else {
+      mockedRequestMetadata =
+          ImmutableMap.of(
+              "Authorization",
+              Collections.singletonList("Bearer " + 
fakeAccessToken.getTokenValue()));
+    }
+    // mock credentials to return the prepared request metadata
+    
Mockito.when(mockedGoogleCredentials.getRequestMetadata()).thenReturn(mockedRequestMetadata);
+
+    // configure environment according to test case
+    String quotaProjectId = testCase.getUserSpecifiedQuotaProjectId(); // 
maybe empty string
+    if (quotaProjectId != null) {
+      // user specified a quota project id
+      System.setProperty(
+          ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getSystemProperty(), 
quotaProjectId);
+    }
+
+    // prepare mock exporter
+    OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = 
Mockito.mock(OtlpGrpcSpanExporter.class);
+    OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
+        Mockito.spy(OtlpGrpcSpanExporter.builder());
+    List<SpanData> exportedSpans = new ArrayList<>();
+    configureGrpcMockSpanExporter(
+        mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, 
exportedSpans);
+
+    try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
+        Mockito.mockStatic(GoogleCredentials.class)) {
+      googleCredentialsMockedStatic
+          .when(GoogleCredentials::getApplicationDefault)
+          .thenReturn(mockedGoogleCredentials);
+
+      // Export telemetry to capture headers in the export calls
+      OpenTelemetrySdk sdk = 
buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
+      generateTestSpan(sdk);
+      CompletableResultCode code = sdk.shutdown();
+      CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
+      assertThat(joinResult.isSuccess()).isTrue();
+      Mockito.verify(spyOtlpGrpcSpanExporterBuilder, Mockito.times(1))
+          .setHeaders(traceHeaderSupplierCaptor.capture());
+
+      // assert that the Authorization bearer token header is present
+      Map<String, String> exportHeaders = 
traceHeaderSupplierCaptor.getValue().get();
+      assertThat(exportHeaders).containsEntry("Authorization", "Bearer fake");
+
+      if (testCase.getExpectedQuotaProjectInHeader() == null) {
+        // there should be no user quota project header
+        assertThat(exportHeaders).doesNotContainKey(QUOTA_USER_PROJECT_HEADER);
+      } else {
+        // there should be user quota project header with expected value
+        assertThat(exportHeaders)
+            .containsEntry(QUOTA_USER_PROJECT_HEADER, 
testCase.getExpectedQuotaProjectInHeader());
+      }
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("provideProjectIdBehaviorTestCases")
+  @SuppressWarnings("CannotMockMethod")
+  void testProjectIdBehavior(ProjectIdTestBehavior testCase) throws 
IOException {
+    
System.clearProperty(ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty());
+    
System.clearProperty(ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty());
+
+    // configure environment according to test case
+    String userSpecifiedProjectId = testCase.getUserSpecifiedProjectId();
+    if (userSpecifiedProjectId != null) {
+      System.setProperty(
+          ConfigurableOption.GOOGLE_CLOUD_PROJECT.getSystemProperty(), 
userSpecifiedProjectId);
+    }
+    System.setProperty(
+        
ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty(), 
SIGNAL_TYPE_TRACES);
+
+    // prepare request metadata (may or may not be called depending on test 
scenario)
+    AccessToken fakeAccessToken = new AccessToken("fake", 
Date.from(Instant.now()));
+    ImmutableMap<String, List<String>> mockedRequestMetadata =
+        ImmutableMap.of(
+            "Authorization",
+            Collections.singletonList("Bearer " + 
fakeAccessToken.getTokenValue()));
+    Mockito.lenient()
+        .when(mockedGoogleCredentials.getRequestMetadata())
+        .thenReturn(mockedRequestMetadata);
+
+    // only mock getProjectId() if it will be called (i.e., user didn't 
specify project ID)
+    boolean shouldFallbackToCredentials =
+        userSpecifiedProjectId == null || userSpecifiedProjectId.isEmpty();
+    if (shouldFallbackToCredentials) {
+      Mockito.when(mockedGoogleCredentials.getProjectId())
+          .thenReturn(testCase.getCredentialsProjectId());
+    }
+
+    // prepare mock exporter
+    OtlpGrpcSpanExporter mockOtlpGrpcSpanExporter = 
Mockito.mock(OtlpGrpcSpanExporter.class);
+    OtlpGrpcSpanExporterBuilder spyOtlpGrpcSpanExporterBuilder =
+        Mockito.spy(OtlpGrpcSpanExporter.builder());
+    List<SpanData> exportedSpans = new ArrayList<>();
+    configureGrpcMockSpanExporter(
+        mockOtlpGrpcSpanExporter, spyOtlpGrpcSpanExporterBuilder, 
exportedSpans);
+
+    try (MockedStatic<GoogleCredentials> googleCredentialsMockedStatic =
+        Mockito.mockStatic(GoogleCredentials.class)) {
+      googleCredentialsMockedStatic
+          .when(GoogleCredentials::getApplicationDefault)
+          .thenReturn(mockedGoogleCredentials);
+
+      if (testCase.getExpectedToThrow()) {
+        // expect exception to be thrown when project ID is not available
+        assertThrows(
+            ConfigurationException.class,
+            () -> buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter));
+        // verify getProjectId() was called to attempt fallback
+        Mockito.verify(mockedGoogleCredentials, 
Mockito.times(1)).getProjectId();
+      } else {
+        // export telemetry and verify resource attributes contain expected 
project ID
+        OpenTelemetrySdk sdk = 
buildOpenTelemetrySdkWithExporter(mockOtlpGrpcSpanExporter);
+        generateTestSpan(sdk);
+        CompletableResultCode code = sdk.shutdown();
+        CompletableResultCode joinResult = code.join(10, TimeUnit.SECONDS);
+        assertThat(joinResult.isSuccess()).isTrue();
+
+        assertThat(exportedSpans).isNotEmpty();
+        for (SpanData spanData : exportedSpans) {
+          assertThat(spanData.getResource().getAttributes().asMap())
+              .containsEntry(
+                  AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY),
+                  testCase.getExpectedProjectIdInResource());
+        }
+
+        // verify whether getProjectId() was called based on whether fallback 
was needed
+        if (shouldFallbackToCredentials) {
+          Mockito.verify(mockedGoogleCredentials, 
Mockito.times(1)).getProjectId();
+        } else {
+          Mockito.verify(mockedGoogleCredentials, 
Mockito.never()).getProjectId();
+        }
+      }
+    }
+  }
+
+  @ParameterizedTest
+  @MethodSource("provideTargetSignalBehaviorTestCases")
+  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<SpanData> 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<MetricData> 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<GoogleCredentials> 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);
+      assertThat(joinResult.isSuccess()).isTrue();
+
+      // 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());
+        
assertThat(traceHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+        
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());
+        
assertThat(metricHeaderSupplierCaptor.getValue().get().size()).isEqualTo(2);
+        
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<Arguments> provideTargetSignalBehaviorTestCases() {
+    return Stream.of(
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("traces")
+                
.setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER)
+                .setExpectedIsMetricsSignalModified(false)
+                .setExpectedIsTraceSignalModified(true)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("metrics")
+                
.setUserSpecifiedOtelProperties(DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER)
+                .setExpectedIsMetricsSignalModified(true)
+                .setExpectedIsTraceSignalModified(false)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("all")
+                .setUserSpecifiedOtelProperties(
+                    ImmutableMap.<String, String>builder()
+                        .put(
+                            "otel.exporter.otlp.metrics.endpoint",
+                            "https://telemetry.googleapis.com/v1/metrics";)
+                        .put(
+                            "otel.exporter.otlp.traces.endpoint",
+                            "https://telemetry.googleapis.com/v1/traces";)
+                        .put("otel.traces.exporter", "otlp")
+                        .put("otel.metrics.exporter", "otlp")
+                        .put("otel.logs.exporter", "none")
+                        .build())
+                .setExpectedIsMetricsSignalModified(true)
+                .setExpectedIsTraceSignalModified(true)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("metrics, traces")
+                .setUserSpecifiedOtelProperties(
+                    ImmutableMap.<String, String>builder()
+                        .put(
+                            "otel.exporter.otlp.metrics.endpoint",
+                            "https://telemetry.googleapis.com/v1/metrics";)
+                        .put(
+                            "otel.exporter.otlp.traces.endpoint",
+                            "https://telemetry.googleapis.com/v1/traces";)
+                        .put("otel.traces.exporter", "otlp")
+                        .put("otel.metrics.exporter", "otlp")
+                        .put("otel.logs.exporter", "none")
+                        .build())
+                .setExpectedIsMetricsSignalModified(true)
+                .setExpectedIsTraceSignalModified(true)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("")
+                .setUserSpecifiedOtelProperties(
+                    ImmutableMap.<String, String>builder()
+                        .put(
+                            "otel.exporter.otlp.metrics.endpoint",
+                            "https://telemetry.googleapis.com/v1/metrics";)
+                        .put(
+                            "otel.exporter.otlp.traces.endpoint",
+                            "https://telemetry.googleapis.com/v1/traces";)
+                        .put("otel.traces.exporter", "otlp")
+                        .put("otel.metrics.exporter", "otlp")
+                        .put("otel.logs.exporter", "none")
+                        .build())
+                .setExpectedIsMetricsSignalModified(true)
+                .setExpectedIsTraceSignalModified(true)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("all")
+                .setUserSpecifiedOtelProperties(
+                    ImmutableMap.of(
+                        "otel.exporter.otlp.metrics.endpoint",
+                        "https://telemetry.googleapis.com/v1/metrics";,
+                        "otel.exporter.otlp.traces.endpoint",
+                        "https://telemetry.googleapis.com/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.<String, String>builder()
+                        .put(
+                            "otel.exporter.otlp.metrics.endpoint",
+                            "https://telemetry.googleapis.com/v1/metrics";)
+                        .put(
+                            "otel.exporter.otlp.traces.endpoint",
+                            "https://telemetry.googleapis.com/v1/traces";)
+                        .put("otel.traces.exporter", "otlp")
+                        .put("otel.metrics.exporter", "otlp")
+                        .put("otel.logs.exporter", "none")
+                        .build())
+                .setExpectedIsMetricsSignalModified(false)
+                .setExpectedIsTraceSignalModified(false)
+                .build()),
+        Arguments.of(
+            TargetSignalBehavior.builder()
+                .setConfiguredTargetSignals("metrics, trace")
+                .setUserSpecifiedOtelProperties(
+                    ImmutableMap.<String, String>builder()
+                        .put(
+                            "otel.exporter.otlp.metrics.endpoint",
+                            "https://telemetry.googleapis.com/v1/metrics";)
+                        .put(
+                            "otel.exporter.otlp.traces.endpoint",
+                            "https://telemetry.googleapis.com/v1/traces";)
+                        .put("otel.traces.exporter", "otlp")
+                        .put("otel.metrics.exporter", "otlp")
+                        .put("otel.logs.exporter", "none")
+                        .build())
+                .setExpectedIsMetricsSignalModified(true)
+                .setExpectedIsTraceSignalModified(false)
+                .build()));
+  }
+
+  /**
+   * Test cases specifying expected value for the project ID in the resource 
given the user input
+   * and the current credentials state.
+   *
+   * <p>{@code null} for {@link 
ProjectIdTestBehavior#getUserSpecifiedProjectId()} indicates the
+   * case of user not specifying the project ID.
+   *
+   * <p>{@code null} value for {@link 
ProjectIdTestBehavior#getCredentialsProjectId()} indicates
+   * that the mocked credentials are not providing a project ID.
+   *
+   * <p>{@code true} for {@link ProjectIdTestBehavior#getExpectedToThrow()} 
indicates the
+   * expectation that an exception should be thrown.
+   */
+  private static Stream<Arguments> provideProjectIdBehaviorTestCases() {
+    return Stream.of(
+        // User specified project ID takes precedence
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId(DUMMY_GCP_RESOURCE_PROJECT_ID)
+                .setCredentialsProjectId("credentials-project-id")
+                .setExpectedProjectIdInResource(DUMMY_GCP_RESOURCE_PROJECT_ID)
+                .setExpectedToThrow(false)
+                .build()),
+        // If user specified project ID is empty, fallback to 
credentials.getProjectId()
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId("")
+                .setCredentialsProjectId("credentials-project-id")
+                .setExpectedProjectIdInResource("credentials-project-id")
+                .setExpectedToThrow(false)
+                .build()),
+        // If user doesn't specify project ID, fallback to 
credentials.getProjectId()
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId(null)
+                .setCredentialsProjectId("credentials-project-id")
+                .setExpectedProjectIdInResource("credentials-project-id")
+                .setExpectedToThrow(false)
+                .build()),
+        // If user doesn't specify and credentials.getProjectId() returns 
null, throw exception
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId(null)
+                .setCredentialsProjectId(null)
+                .setExpectedProjectIdInResource(null)
+                .setExpectedToThrow(true)
+                .build()),
+        // If user specified project ID is empty and 
credentials.getProjectId() returns null, throw
+        // exception
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId("")
+                .setCredentialsProjectId(null)
+                .setExpectedProjectIdInResource(null)
+                .setExpectedToThrow(true)
+                .build()),
+        // If user specifies empty and credentials returns empty (edge case), 
throw exception
+        Arguments.of(
+            ProjectIdTestBehavior.builder()
+                .setUserSpecifiedProjectId("")
+                .setCredentialsProjectId("")
+                .setExpectedProjectIdInResource(null)
+                .setExpectedToThrow(true)
+                .build()));
+  }
+
+  /**
+   * Test cases specifying expected value for the user quota project header 
given the user input and
+   * the current credentials state.
+   *
+   * <p>{@code null} for {@link 
QuotaProjectIdTestBehavior#getUserSpecifiedQuotaProjectId()}
+   * indicates the case of user not specifying the quota project ID.
+   *
+   * <p>{@code null} value for {@link 
QuotaProjectIdTestBehavior#getExpectedQuotaProjectInHeader()}
+   * indicates the expectation that the QUOTA_USER_PROJECT_HEADER should not 
be present in the
+   * export headers.
+   *
+   * <p>{@code true} for {@link 
QuotaProjectIdTestBehavior#getIsQuotaProjectPresentInMetadata()}
+   * indicates that the mocked credentials are configured to provide 
DUMMY_GCP_QUOTA_PROJECT_ID as
+   * the quota project ID.
+   */
+  private static Stream<Arguments> provideQuotaBehaviorTestCases() {
+    return Stream.of(
+        // If quota project present in metadata, it will be used
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .setIsQuotaProjectPresentInMetadata(true)
+                .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .build()),
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
+                .setIsQuotaProjectPresentInMetadata(true)
+                .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .build()),
+        // If quota project not present in request metadata, then user 
specified project is used
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .setIsQuotaProjectPresentInMetadata(false)
+                .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .build()),
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId("my-custom-quota-project-id")
+                .setIsQuotaProjectPresentInMetadata(false)
+                .setExpectedQuotaProjectInHeader("my-custom-quota-project-id")
+                .build()),
+        // Testing for special edge case inputs
+        // user-specified quota project is empty
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId("") // user explicitly 
specifies empty
+                .setIsQuotaProjectPresentInMetadata(true)
+                .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .build()),
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId("")
+                .setIsQuotaProjectPresentInMetadata(false)
+                .setExpectedQuotaProjectInHeader(null)
+                .build()),
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId(null) // user omits specifying 
quota project
+                .setIsQuotaProjectPresentInMetadata(true)
+                .setExpectedQuotaProjectInHeader(DUMMY_GCP_QUOTA_PROJECT_ID)
+                .build()),
+        Arguments.of(
+            QuotaProjectIdTestBehavior.builder()
+                .setUserSpecifiedQuotaProjectId(null)
+                .setIsQuotaProjectPresentInMetadata(false)
+                .setExpectedQuotaProjectInHeader(null)
+                .build()));
+  }
+
+  // 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<SpanData> exportedSpanContainer) {
+    
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));
+              return CompletableResultCode.ofSuccess();
+            });
+  }
+
+  // Configure necessary behavior on the http mock metric exporters to work.
+  private static void configureHttpMockMetricExporter(
+      OtlpHttpMetricExporter mockOtlpHttpMetricExporter,
+      OtlpHttpMetricExporterBuilder spyOtlpHttpMetricExporterBuilder,
+      List<MetricData> 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<Aggregation>)
+                invocationOnMock -> {
+                  InstrumentType instrumentType = 
invocationOnMock.getArgument(0);
+                  return 
OtlpHttpMetricExporter.getDefault().getDefaultAggregation(instrumentType);
+                });
+    
Mockito.when(mockOtlpHttpMetricExporter.getAggregationTemporality(Mockito.any()))
+        .thenAnswer(
+            (Answer<AggregationTemporality>)
+                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<MetricData> 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<Aggregation>)
+                invocationOnMock -> {
+                  InstrumentType instrumentType = 
invocationOnMock.getArgument(0);
+                  return 
OtlpGrpcMetricExporter.getDefault().getDefaultAggregation(instrumentType);
+                });
+    Mockito.lenient()
+        
.when(mockOtlpGrpcMetricExporter.getAggregationTemporality(Mockito.any()))
+        .thenAnswer(
+            (Answer<AggregationTemporality>)
+                invocationOnMock -> {
+                  InstrumentType instrumentType = 
invocationOnMock.getArgument(0);
+                  return OtlpGrpcMetricExporter.getDefault()
+                      .getAggregationTemporality(instrumentType);
+                });
+    Mockito.lenient()
+        .when(mockOtlpGrpcMetricExporter.getMemoryMode())
+        .thenReturn(MemoryMode.IMMUTABLE_DATA);
+  }
+
+  @AutoValue
+  abstract static class ProjectIdTestBehavior {
+    // A null user specified project ID represents the use case where user 
omits specifying it
+    @Nullable
+    abstract String getUserSpecifiedProjectId();
+
+    // The project ID that credentials.getProjectId() returns (can be null)
+    @Nullable
+    abstract String getCredentialsProjectId();
+
+    // The expected project ID in the resource attributes (null if exception 
expected)
+    @Nullable
+    abstract String getExpectedProjectIdInResource();
+
+    // Whether an exception is expected to be thrown
+    abstract boolean getExpectedToThrow();
+
+    static Builder builder() {
+      return new 
AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_ProjectIdTestBehavior
+          .Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder setUserSpecifiedProjectId(String projectId);
+
+      abstract Builder setCredentialsProjectId(String projectId);
+
+      abstract Builder setExpectedProjectIdInResource(String projectId);
+
+      abstract Builder setExpectedToThrow(boolean expectedToThrow);
+
+      abstract ProjectIdTestBehavior build();
+    }
+  }
+
+  @AutoValue
+  abstract static class QuotaProjectIdTestBehavior {
+    // A null user specified quota represents the use case where user omits 
specifying quota
+    @Nullable
+    abstract String getUserSpecifiedQuotaProjectId();
+
+    abstract boolean getIsQuotaProjectPresentInMetadata();
+
+    // If expected quota project in header is null, the header entry should 
not be present in export
+    @Nullable
+    abstract String getExpectedQuotaProjectInHeader();
+
+    static Builder builder() {
+      return new 
AutoValue_GcpAuthAutoConfigurationCustomizerProviderTest_QuotaProjectIdTestBehavior
+          .Builder();
+    }
+
+    @AutoValue.Builder
+    abstract static class Builder {
+      abstract Builder setUserSpecifiedQuotaProjectId(String quotaProjectId);
+
+      abstract Builder setIsQuotaProjectPresentInMetadata(boolean 
quotaProjectPresentInMetadata);
+
+      /**
+       * Sets the expected quota project header value for the test case. A 
null value is allowed,
+       * and it indicates that the header should not be present in the export 
request.
+       *
+       * @param expectedQuotaProjectInHeader the expected header value to 
match in the export
+       *     headers.
+       */
+      abstract Builder setExpectedQuotaProjectInHeader(String 
expectedQuotaProjectInHeader);
+
+      abstract QuotaProjectIdTestBehavior build();
+    }
+  }
+
+  @AutoValue
+  abstract static class TargetSignalBehavior {
+    @Nonnull
+    abstract String getConfiguredTargetSignals();
+
+    @Nonnull
+    abstract ImmutableMap<String, String> 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<String, String> 
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.lenient()
+          .when(mockedGoogleCredentials.getRequestMetadata())
+          .thenReturn(
+              ImmutableMap.of(
+                  "Authorization",
+                  Collections.singletonList("Bearer " + 
fakeAccessToken.getTokenValue()),
+                  QUOTA_USER_PROJECT_HEADER,
+                  Collections.singletonList(DUMMY_GCP_QUOTA_PROJECT_ID)));
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(SpanExporter 
spanExporter) {
+    return buildOpenTelemetrySdkWithExporter(
+        spanExporter, OtlpHttpMetricExporter.getDefault(), 
DEFAULT_OTEL_PROPERTIES_SPAN_EXPORTER);
+  }
+
+  @SuppressWarnings("UnusedMethod")
+  private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(
+      SpanExporter spanExporter, ImmutableMap<String, String> 
customOTelProperties) {
+    return buildOpenTelemetrySdkWithExporter(
+        spanExporter, OtlpHttpMetricExporter.getDefault(), 
customOTelProperties);
+  }
+
+  private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(MetricExporter 
metricExporter) {
+    return buildOpenTelemetrySdkWithExporter(
+        OtlpHttpSpanExporter.getDefault(), metricExporter, 
DEFAULT_OTEL_PROPERTIES_METRIC_EXPORTER);
+  }
+
+  @SuppressWarnings("UnusedMethod")
+  private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(
+      MetricExporter metricExporter, ImmutableMap<String, String> 
customOtelProperties) {
+    return buildOpenTelemetrySdkWithExporter(
+        OtlpHttpSpanExporter.getDefault(), metricExporter, 
customOtelProperties);
+  }
+
+  private OpenTelemetrySdk buildOpenTelemetrySdkWithExporter(
+      SpanExporter spanExporter,
+      MetricExporter metricExporter,
+      ImmutableMap<String, String> customOtelProperties) {
+    SpiHelper spiHelper =
+        
SpiHelper.create(GcpAuthAutoConfigurationCustomizerProviderTest.class.getClassLoader());
+    AutoConfiguredOpenTelemetrySdkBuilder builder =
+        AutoConfiguredOpenTelemetrySdk.builder()
+            .addPropertiesSupplier(() -> customOtelProperties)
+            .setComponentLoader(
+                new ComponentLoader() {
+                  @Override
+                  public <T> List<T> load(Class<T> spiClass) {
+                    if (spiClass == ConfigurableSpanExporterProvider.class) {
+                      return Collections.singletonList(
+                          spiClass.cast(
+                              new ConfigurableSpanExporterProvider() {
+                                @Override
+                                public SpanExporter createExporter(
+                                    ConfigProperties configProperties) {
+                                  return spanExporter;
+                                }
+
+                                @Override
+                                public String getName() {
+                                  return "otlp";
+                                }
+                              }));
+                    }
+                    if (spiClass == ConfigurableMetricExporterProvider.class) {
+                      return Collections.singletonList(
+                          spiClass.cast(
+                              new ConfigurableMetricExporterProvider() {
+                                @Override
+                                public MetricExporter createExporter(
+                                    ConfigProperties configProperties) {
+                                  return metricExporter;
+                                }
+
+                                @Override
+                                public String getName() {
+                                  return "otlp";
+                                }
+                              }));
+                    }
+                    return spiHelper.load(spiClass);
+                  }
+                });
+
+    return builder.build().getOpenTelemetrySdk();
+  }
+
+  private static boolean authHeadersQuotaProjectIsPresent(Map<String, String> 
headers) {
+    Set<Entry<String, String>> headerEntrySet = headers.entrySet();
+    return headerEntrySet.contains(
+            new SimpleEntry<>(
+                QUOTA_USER_PROJECT_HEADER,
+                
GcpAuthAutoConfigurationCustomizerProviderTest.DUMMY_GCP_QUOTA_PROJECT_ID))
+        && headerEntrySet.contains(new SimpleEntry<>("Authorization", "Bearer 
fake"));
+  }
+
+  private static void generateTestSpan(OpenTelemetrySdk openTelemetrySdk) {
+    Span span = 
openTelemetrySdk.getTracer("test").spanBuilder("sample").startSpan();
+    try (Scope ignored = span.makeCurrent()) {
+      long workOutput = busyloop();
+      span.setAttribute("work_loop", workOutput);
+    } finally {
+      span.end();
+    }
+  }
+
+  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();
+    Instant end;
+    long counter = 0;
+    do {
+      counter++;
+      end = Instant.now();
+    } while (Duration.between(start, end).toMillis() < 1000);
+    return counter;
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 603832045f3..443d9c56775 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -192,6 +192,7 @@ include(":sdks:java:extensions:avro")
 include(":sdks:java:extensions:euphoria")
 include(":sdks:java:extensions:kryo")
 include(":sdks:java:extensions:google-cloud-platform-core")
+include(":sdks:java:extensions:opentelemetry-gcp-auth-extension")
 include(":sdks:java:extensions:jackson")
 include(":sdks:java:extensions:join-library")
 include(":sdks:java:extensions:kafka-factories")


Reply via email to