getConfigSources(final ConfigSourceContext context) {
ConfigValue enableMetadataServer = context.getValue("quarkus.google.cloud.enable-metadata-server");
if (enableMetadataServer.getValue() != null) {
if (Converters.getImplicitConverter(Boolean.class).convert(enableMetadataServer.getValue())) {
- String previousContextStorageSysProp = null;
- try {
- // Google HTTP Client under the hood which attempts to record traces via OpenCensus which is wired
- // to delegate to OpenTelemetry.
- // This can lead to problems with the Quarkus OpenTelemetry extension which expects Vert.x to be running,
- // something that is not the case at build time, see https://github.com/quarkusio/quarkus/issues/35500
- previousContextStorageSysProp = System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP,
- "default");
-
- String defaultProjectId = ServiceOptions.getDefaultProjectId();
- if (defaultProjectId != null) {
- return singletonList(
- new PropertiesConfigSource(Map.of("quarkus.google.cloud.project-id", defaultProjectId),
- "GcpDefaultsConfigSource",
- -Integer.MAX_VALUE));
- }
- } finally {
- if (previousContextStorageSysProp == null) {
- System.clearProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP);
- } else {
- System.setProperty(OPENTELEMETRY_CONTEXT_CONTEXT_STORAGE_PROVIDER_SYS_PROP,
- previousContextStorageSysProp);
+ String defaultProjectId = defaultProjectIdSupplier.get();
+ if (defaultProjectId != null) {
+ return singletonList(
+ new PropertiesConfigSource(Map.of("quarkus.google.cloud.project-id", defaultProjectId),
+ "GcpDefaultsConfigSource",
+ -Integer.MAX_VALUE));
+ }
+ }
+ }
+ return emptyList();
+ }
+
+ /**
+ * This is a partial copy of {@link ServiceOptions} to prevent the use of Google's HTTP client, which causes
+ * static initialization trouble via OpenCensus-shim with OpenTelemetry.
+ *
+ *
+ * This helper class is only intended to not use the Google HTTP client but not change any other aspects of
+ * how the default project ID is retrieved.
+ *
+ *
+ * (The {@link ServiceOptions} class is licensed using ASL2.)
+ */
+ @SuppressWarnings("rawtypes")
+ static class ServiceOptionsHelper extends ServiceOptions {
+ @SuppressWarnings("unchecked")
+ protected ServiceOptionsHelper(Class serviceFactoryClass, Class rpcFactoryClass, Builder builder,
+ ServiceDefaults serviceDefaults) {
+ super(serviceFactoryClass, rpcFactoryClass, builder, serviceDefaults);
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected Set getScopes() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Builder toBuilder() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static String getDefaultProjectId() {
+ // As in the original `ServiceOptions` class
+ String projectId = System.getProperty("GOOGLE_CLOUD_PROJECT", System.getenv("GOOGLE_CLOUD_PROJECT"));
+ if (projectId == null) {
+ projectId = System.getProperty("GCLOUD_PROJECT", System.getenv("GCLOUD_PROJECT"));
+ }
+
+ if (projectId == null) {
+ projectId = getAppEngineProjectId();
+ }
+
+ if (projectId == null) {
+ projectId = getServiceAccountProjectId();
+ }
+
+ return projectId != null ? projectId : getGoogleCloudProjectId();
+ }
+
+ protected static String getAppEngineProjectId() {
+ // As in the original `ServiceOptions` class
+ String projectId;
+ if (PlatformInformation.isOnGAEStandard7()) {
+ projectId = getAppEngineProjectIdFromAppId();
+ } else {
+ projectId = System.getenv("GOOGLE_CLOUD_PROJECT");
+ if (projectId == null) {
+ projectId = System.getenv("GCLOUD_PROJECT");
+ }
+
+ if (projectId == null) {
+ projectId = getAppEngineProjectIdFromAppId();
+ }
+
+ if (projectId == null) {
+ try {
+ projectId = getAppEngineProjectIdFromMetadataServer();
+ } catch (IOException var2) {
+ // projectId = null;
}
}
+ }
+
+ return projectId;
+ }
+ /**
+ * This function has been changed to use the (new) Java HTTP client.
+ */
+ private static String getAppEngineProjectIdFromMetadataServer() throws IOException {
+ String metadata = "http://metadata.google.internal";
+ String projectIdURL = "/computeMetadata/v1/project/project-id";
+
+ try {
+ URI uri = new URI(metadata + projectIdURL);
+ HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofMillis(500)).build();
+ HttpRequest request = HttpRequest.newBuilder().timeout(Duration.ofMillis(500)).GET().uri(uri)
+ .header("Metadata-Flavor", "Google").build();
+ HttpResponse.BodyHandler bodyHandler = HttpResponse.BodyHandlers.ofString();
+ HttpResponse response = client.send(request, bodyHandler);
+ return headerContainsMetadataFlavor(response) ? response.body() : null;
+ } catch (URISyntaxException e) {
+ throw new RuntimeException(e);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IOException(e);
}
}
- return emptyList();
+
+ /**
+ * This function has been adopted for the Java HTTP client.
+ */
+ private static boolean headerContainsMetadataFlavor(HttpResponse> response) {
+ String metadataFlavorValue = response.headers().firstValue("Metadata-Flavor").orElse("");
+ return "Google".equals(metadataFlavorValue);
+ }
}
}
diff --git a/common/runtime/src/test/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigSourceFactoryTest.java b/common/runtime/src/test/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigSourceFactoryTest.java
new file mode 100644
index 00000000..38311005
--- /dev/null
+++ b/common/runtime/src/test/java/io/quarkiverse/googlecloudservices/common/GcpDefaultsConfigSourceFactoryTest.java
@@ -0,0 +1,63 @@
+package io.quarkiverse.googlecloudservices.common;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.OpenTelemetry;
+import io.smallrye.config.ConfigSourceContext;
+import io.smallrye.config.ConfigValue;
+
+class GcpDefaultsConfigSourceFactoryTest {
+ @Test
+ void configSourceWorks() {
+ ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
+ Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
+ .thenReturn(ConfigValue.builder().withValue("true").build());
+
+ Iterable configSources = new GcpDefaultsConfigSourceFactory(() -> "test-project-id")
+ .getConfigSources(context);
+ assertThat(configSources).asList().hasSize(1);
+
+ ConfigSource configSource = configSources.iterator().next();
+ assertThat(configSource.getProperties()).containsEntry("quarkus.google.cloud.project-id", "test-project-id");
+ }
+
+ @Test
+ void metadataServerDisabled() {
+ ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
+ Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
+ .thenReturn(ConfigValue.builder().withValue("false").build());
+
+ Iterable configSources = new GcpDefaultsConfigSourceFactory(() -> "test-project-id")
+ .getConfigSources(context);
+ assertThat(configSources).isEmpty();
+ }
+
+ /**
+ * Tests that OpenCensus does not get implicitly initialized and in turn does not "collide" with
+ * OpenTelemetry, as used in Quarkus.
+ */
+ @Test
+ void staticOpenCensusOpenTelemetryInit() {
+ try {
+ GlobalOpenTelemetry.resetForTest();
+
+ ConfigSourceContext context = Mockito.mock(ConfigSourceContext.class);
+ Mockito.when(context.getValue("quarkus.google.cloud.enable-metadata-server"))
+ .thenReturn(ConfigValue.builder().withValue("true").build());
+
+ // Uses the "real" implementation that tries to fetch the default-project-id
+ Iterable configSources = new GcpDefaultsConfigSourceFactory().getConfigSources(context);
+ assertThat(configSources).asList().hasSizeLessThanOrEqualTo(1);
+
+ // This is a pretty ugly way, because it changes static state
+ GlobalOpenTelemetry.set(OpenTelemetry.noop());
+ } finally {
+ GlobalOpenTelemetry.resetForTest();
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index f12003c3..81543d14 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,6 +21,7 @@
3.8.1
3.4.1
3.24.2
+ 1.28.0-alpha
scm:git:git@github.com:quarkiverse/quarkus-google-cloud-services.git
@@ -63,6 +64,11 @@
assertj-core
${assertj.version}
+
+ io.opentelemetry
+ opentelemetry-opencensus-shim
+ ${opentelemetry-alpha.version}
+