From 60dfec0bca5502efeb4bff50dfe621d74f6260e9 Mon Sep 17 00:00:00 2001 From: Pranav Sharma Date: Tue, 16 Jan 2024 16:07:17 -0500 Subject: [PATCH] Add new module for resource-detector support library (#276) * Add new module for resource-detector support library * Rname GCPResource -> GCPResourceProvider * Extract GCPMetadataConfig into support library * Refactor platform detection from resource provider * Refactor platform detection to enhance testing * make EnvironmentVariables package-private * Add license headers to the newly added files * Fix spotless style findings * Remove CloudLocationUtil class * change attribute map to hold string instead of optional values * add tests to verify resource attributes mapping * add javadoc to public classes * add unit test for ServiceLoader discoverability * Remove no-args constructor for simplified testing * Update gradle files to support junit5 testing * Add missing attribute in GKE mapping * Add tests for resources-support library * Update copyright year in GoogleCloudRun.java * Add missing newline * Update resource-detector tests to JUnit5 * Remove non-GCP attributes from GKE detection * Add missing attributes in detected GCE environment * Update tests for GAE * Add CLOUD_ACCOUNT_ID to all supported platforms * Rename support package: detectors -> detection Support library was using the same package as the detectors library. This rename would avoid potential conflicts and import issues. * Undo breaking changes These breaking chnages can be directly introduced when we contribute the resource detector upstream. This commit adds classes back and marks them deprecated to indicate to the users not to depend on them. --- build.gradle | 6 +- detectors/resources-support/build.gradle | 36 ++ .../detection/AttributeKeys.java | 60 ++ .../detection/DetectedPlatform.java | 46 ++ .../detection/EnvironmentVariables.java | 33 ++ .../detection/GCPMetadataConfig.java | 168 ++++++ .../detection/GCPPlatformDetector.java | 108 ++++ .../detection/GoogleAppEngine.java | 72 +++ .../detection/GoogleCloudFunction.java | 27 + .../detection/GoogleCloudRun.java | 27 + .../detection/GoogleComputeEngine.java | 63 ++ .../detection/GoogleKubernetesEngine.java | 75 +++ .../detection/GoogleServerlessCompute.java | 57 ++ .../detection/UnknownPlatform.java | 39 ++ .../opentelemetry/detection}/EnvVarMock.java | 6 +- .../detection/GCPMetadataConfigTest.java | 151 +++++ .../detection/GCPPlatformDetectorTest.java | 358 ++++++++++++ .../opentelemetry/detection/TestUtils.java | 31 + detectors/resources/build.gradle | 12 +- .../detectors/AttributesExtractorUtil.java | 3 + .../opentelemetry/detectors/EnvVars.java | 4 + .../detectors/GCPMetadataConfig.java | 3 + .../opentelemetry/detectors/GCPResource.java | 322 ++++------- .../detectors/GCPResourceTest.java | 546 +++++++++++------- .../example/resource/ResourceExample.java | 4 +- settings.gradle | 4 + 26 files changed, 1851 insertions(+), 410 deletions(-) create mode 100644 detectors/resources-support/build.gradle create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/DetectedPlatform.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/EnvironmentVariables.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleAppEngine.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudFunction.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRun.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleComputeEngine.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleKubernetesEngine.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleServerlessCompute.java create mode 100644 detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/UnknownPlatform.java rename detectors/{resources/src/test/java/com/google/cloud/opentelemetry/detectors => resources-support/src/test/java/com/google/cloud/opentelemetry/detection}/EnvVarMock.java (85%) create mode 100644 detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfigTest.java create mode 100644 detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java create mode 100644 detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/TestUtils.java diff --git a/build.gradle b/build.gradle index c36575ea..dc878c04 100644 --- a/build.gradle +++ b/build.gradle @@ -147,10 +147,11 @@ subprojects { openTelemetryInstrumentationVersion = '1.31.0' openTelemetrySemconvVersion = '1.22.0' junitVersion = '4.13' + junit5Version = '5.10.0' mockitoVersion = '3.5.10' pubSubVersion = '1.125.11' testContainersVersion = '1.15.1' - wiremockVersion = '2.27.2' + wiremockVersion = '2.35.0' springWebVersion = '2.4.5' springOpenFeignVersion = '3.0.0' springOtelVersion = '1.0.0-M8' @@ -194,6 +195,9 @@ subprojects { testLibraries = [ assertj : "org.assertj:assertj-core:${assertjVersion}", junit : "junit:junit:${junitVersion}", + junit5 : "org.junit.jupiter:junit-jupiter-api:${junit5Version}", + junit5_runtime : "org.junit.jupiter:junit-jupiter-engine:${junit5Version}", + junit5_params : "org.junit.jupiter:junit-jupiter-params:${junit5Version}", mockito : "org.mockito:mockito-inline:${mockitoVersion}", slf4j_simple: "org.slf4j:slf4j-simple:${slf4jVersion}", opentelemetry_sdk_testing: "io.opentelemetry:opentelemetry-sdk-testing:${openTelemetryVersion}", diff --git a/detectors/resources-support/build.gradle b/detectors/resources-support/build.gradle new file mode 100644 index 00000000..b7022a5e --- /dev/null +++ b/detectors/resources-support/build.gradle @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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. + */ +description = 'Support library for Google Cloud Resource Detector' + +dependencies { + testImplementation(testLibraries.assertj) + testImplementation(testLibraries.wiremock) + testImplementation(testLibraries.mockito) + testImplementation(testLibraries.junit5) + testImplementation(testLibraries.junit5_params) + testRuntimeOnly(testLibraries.junit5_runtime) +} + +afterEvaluate { + tasks.named("compileJava"){ + options.release = 8 + } +} + +test { + // required for discovering JUnit 5 tests + useJUnitPlatform() +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java new file mode 100644 index 00000000..c7a45ac8 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/AttributeKeys.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +/** + * Contains constants that act as keys for the known attributes for {@link + * GCPPlatformDetector.SupportedPlatform}s. + */ +public final class AttributeKeys { + // GCE Attributes + public static final String GCE_AVAILABILITY_ZONE = AttributeKeys.AVAILABILITY_ZONE; + public static final String GCE_CLOUD_REGION = AttributeKeys.CLOUD_REGION; + public static final String GCE_INSTANCE_ID = AttributeKeys.INSTANCE_ID; + public static final String GCE_INSTANCE_NAME = AttributeKeys.INSTANCE_NAME; + public static final String GCE_MACHINE_TYPE = AttributeKeys.MACHINE_TYPE; + public static final String GCE_INSTANCE_HOSTNAME = "instance_hostname"; + + // GKE Attributes + public static final String GKE_CLUSTER_NAME = "gke_cluster_name"; + public static final String GKE_CLUSTER_LOCATION_TYPE = "gke_cluster_location_type"; + public static final String GKE_CLUSTER_LOCATION = "gke_cluster_location"; + public static final String GKE_HOST_ID = AttributeKeys.INSTANCE_ID; + + // GKE Location Constants + public static final String GKE_LOCATION_TYPE_ZONE = "ZONE"; + public static final String GKE_LOCATION_TYPE_REGION = "REGION"; + + // GAE Attributes + public static final String GAE_MODULE_NAME = "gae_module_name"; + public static final String GAE_APP_VERSION = "gae_app_version"; + public static final String GAE_INSTANCE_ID = AttributeKeys.INSTANCE_ID; + public static final String GAE_AVAILABILITY_ZONE = AttributeKeys.AVAILABILITY_ZONE; + public static final String GAE_CLOUD_REGION = AttributeKeys.CLOUD_REGION; + + // Google Serverless Compute Attributes + public static final String SERVERLESS_COMPUTE_NAME = "serverless_compute_name"; + public static final String SERVERLESS_COMPUTE_REVISION = "serverless_compute_revision"; + public static final String SERVERLESS_COMPUTE_AVAILABILITY_ZONE = AttributeKeys.AVAILABILITY_ZONE; + public static final String SERVERLESS_COMPUTE_CLOUD_REGION = AttributeKeys.CLOUD_REGION; + public static final String SERVERLESS_COMPUTE_INSTANCE_ID = AttributeKeys.INSTANCE_ID; + + static final String AVAILABILITY_ZONE = "availability_zone"; + static final String CLOUD_REGION = "cloud_region"; + static final String INSTANCE_ID = "instance_id"; + static final String INSTANCE_NAME = "instance_name"; + static final String MACHINE_TYPE = "machine_type"; +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/DetectedPlatform.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/DetectedPlatform.java new file mode 100644 index 00000000..019927e3 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/DetectedPlatform.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import java.util.Map; + +/** Represents a GCP specific platform on which a cloud application can run. */ +public interface DetectedPlatform { + /** + * Method to retrieve the underlying compute platform on which application is running. + * + * @return the {@link GCPPlatformDetector.SupportedPlatform} representing the Google Cloud + * platform on which application is running. + */ + GCPPlatformDetector.SupportedPlatform getSupportedPlatform(); + + /** + * Method to retrieve the GCP Project ID in which the GCP specific platform exists. Every valid + * platform must have a GCP Project ID associated with it. + * + * @return the Google Cloud project ID. + */ + String getProjectId(); + + /** + * Method to retrieve the attributes associated with the compute platform on which the application + * is running as key-value pairs. The valid keys to query on this {@link Map} are specified in the + * {@link AttributeKeys}. + * + * @return a {@link Map} of attributes specific to the underlying compute platform. + */ + Map getAttributes(); +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/EnvironmentVariables.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/EnvironmentVariables.java new file mode 100644 index 00000000..e5d643e9 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/EnvironmentVariables.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +/** + * Provides API to fetch environment variables. This is useful in order to create a mock class for + * testing. + */ +interface EnvironmentVariables { + /** Returns the current environment variables of the platform this is running in. */ + EnvironmentVariables DEFAULT_INSTANCE = System::getenv; + + /** + * Grabs the system environment variable. Returns null on failure. + * + * @param key the key of the environment variable in {@code System.getenv()} + * @return the value received by {@code System.getenv(key)} + */ + String get(String key); +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java new file mode 100644 index 00000000..09cf39f4 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfig.java @@ -0,0 +1,168 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * Retrieves Google Cloud project-id and a limited set of instance attributes from Metadata server. + * + * @see + * https://cloud.google.com/compute/docs/storing-retrieving-metadata + */ +final class GCPMetadataConfig { + static final GCPMetadataConfig DEFAULT_INSTANCE = new GCPMetadataConfig(); + + private static final String DEFAULT_URL = "http://metadata.google.internal/computeMetadata/v1/"; + private final String url; + private final Map cachedAttributes = new HashMap<>(); + + private GCPMetadataConfig() { + this.url = DEFAULT_URL; + } + + // For testing only + GCPMetadataConfig(String url) { + this.url = url; + } + + // Returns null on failure to retrieve from metadata server + String getProjectId() { + return getAttribute("project/project-id"); + } + + /** + * Method to extract cloud availability zone from the metadata server. + * + *

Example response: projects/640212054955/zones/australia-southeast1-a + * + *

Example zone: australia-southeast1-a + * + * @return the extracted zone from the metadata server response or null in case of failure to + * retrieve from metadata server. + */ + String getZone() { + String zone = getAttribute("instance/zone"); + if (zone != null && zone.contains("/")) { + zone = zone.substring(zone.lastIndexOf('/') + 1); + } + return zone; + } + + /** + * Use this method only when the region cannot be parsed from the zone. Known use-cases of this + * method involve detecting region in GAE standard environment. + * + *

Example response: projects/5689182099321/regions/us-central1. + * + * @return the retrieved region or null in case of failure to retrieve from metadata server + */ + String getRegion() { + String region = getAttribute("instance/region"); + if (region != null && region.contains("/")) { + region = region.substring(region.lastIndexOf('/') + 1); + } + return region; + } + + /** + * Use this method to parse region from zone. + * + *

Example region: australia-southeast1 + * + * @return parsed region from the zone, if zone is not found or is invalid, this method returns + * null. + */ + String getRegionFromZone() { + String region = null; + String zone = getZone(); + if (zone != null && !zone.isEmpty()) { + // Parsing required to scope up to a region + String[] splitArr = zone.split("-"); + if (splitArr.length > 2) { + region = String.join("-", splitArr[0], splitArr[1]); + } + } + return region; + } + + // Example response: projects/640212054955/machineTypes/e2-medium + String getMachineType() { + String machineType = getAttribute("instance/machine-type"); + if (machineType != null && machineType.contains("/")) { + machineType = machineType.substring(machineType.lastIndexOf('/') + 1); + } + return machineType; + } + + // Returns null on failure to retrieve from metadata server + String getInstanceId() { + return getAttribute("instance/id"); + } + + // Returns null on failure to retrieve from metadata server + String getClusterName() { + return getAttribute("instance/attributes/cluster-name"); + } + + // Returns null on failure to retrieve from metadata server + String getClusterLocation() { + return getAttribute("instance/attributes/cluster-location"); + } + + // Returns null on failure to retrieve from metadata server + String getInstanceHostName() { + return getAttribute("instance/hostname"); + } + + // Returns null on failure to retrieve from metadata server + String getInstanceName() { + return getAttribute("instance/name"); + } + + // Returns null on failure to retrieve from metadata server + private String getAttribute(String attributeName) { + return cachedAttributes.computeIfAbsent(attributeName, this::fetchAttribute); + } + + // Return the attribute received at relative path or null on failure + private String fetchAttribute(String attributeName) { + try { + URL url = new URL(this.url + attributeName); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestProperty("Metadata-Flavor", "Google"); + if (connection.getResponseCode() == 200 + && ("Google").equals(connection.getHeaderField("Metadata-Flavor"))) { + InputStream input = connection.getInputStream(); + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + return reader.readLine(); + } + } + } catch (IOException ignore) { + // ignore + } + return null; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java new file mode 100644 index 00000000..414f9b56 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetector.java @@ -0,0 +1,108 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +public class GCPPlatformDetector { + public static final GCPPlatformDetector DEFAULT_INSTANCE = new GCPPlatformDetector(); + + private final GCPMetadataConfig metadataConfig; + private final EnvironmentVariables environmentVariables; + + // for testing only + GCPPlatformDetector(GCPMetadataConfig metadataConfig, EnvironmentVariables environmentVariables) { + this.metadataConfig = metadataConfig; + this.environmentVariables = environmentVariables; + } + + private GCPPlatformDetector() { + this.metadataConfig = GCPMetadataConfig.DEFAULT_INSTANCE; + this.environmentVariables = EnvironmentVariables.DEFAULT_INSTANCE; + } + + /** + * Detects the GCP platform on which the application is running. + * + * @return the specific GCP platform on which the application is running. + */ + public DetectedPlatform detectPlatform() { + return generateDetectedPlatform(detectSupportedPlatform()); + } + + private SupportedPlatform detectSupportedPlatform() { + if (!isRunningOnGcp()) { + return SupportedPlatform.UNKNOWN_PLATFORM; + } + // Note: Order of detection matters here + if (environmentVariables.get("KUBERNETES_SERVICE_HOST") != null) { + return SupportedPlatform.GOOGLE_KUBERNETES_ENGINE; + } else if (environmentVariables.get("K_CONFIGURATION") != null + && environmentVariables.get("FUNCTION_TARGET") == null) { + return SupportedPlatform.GOOGLE_CLOUD_RUN; + } else if (environmentVariables.get("FUNCTION_TARGET") != null) { + return SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS; + } else if (environmentVariables.get("GAE_SERVICE") != null) { + return SupportedPlatform.GOOGLE_APP_ENGINE; + } + return SupportedPlatform.GOOGLE_COMPUTE_ENGINE; // default to GCE + } + + private boolean isRunningOnGcp() { + return metadataConfig.getProjectId() != null && !metadataConfig.getProjectId().isEmpty(); + } + + private DetectedPlatform generateDetectedPlatform(SupportedPlatform platform) { + DetectedPlatform detectedPlatform; + switch (platform) { + case GOOGLE_KUBERNETES_ENGINE: + detectedPlatform = new GoogleKubernetesEngine(metadataConfig); + break; + case GOOGLE_CLOUD_RUN: + detectedPlatform = new GoogleCloudRun(environmentVariables, metadataConfig); + break; + case GOOGLE_CLOUD_FUNCTIONS: + detectedPlatform = new GoogleCloudFunction(environmentVariables, metadataConfig); + break; + case GOOGLE_APP_ENGINE: + detectedPlatform = new GoogleAppEngine(environmentVariables, metadataConfig); + break; + case GOOGLE_COMPUTE_ENGINE: + detectedPlatform = new GoogleComputeEngine(metadataConfig); + break; + default: + detectedPlatform = new UnknownPlatform(); + } + return detectedPlatform; + } + + /** + * SupportedPlatform represents the GCP platforms that can currently be detected by the + * resource-detector. + */ + public enum SupportedPlatform { + /** Represents the Google Compute Engine platform. */ + GOOGLE_COMPUTE_ENGINE, + /** Represents the Google Kubernetes Engine platform. */ + GOOGLE_KUBERNETES_ENGINE, + /** Represents the Google App Engine platform. Could either be flex or standard. */ + GOOGLE_APP_ENGINE, + /** Represents the Google Cloud Run platform. */ + GOOGLE_CLOUD_RUN, + /** Represents the Google Cloud Functions platform. */ + GOOGLE_CLOUD_FUNCTIONS, + /** Represents the case when the application is not running on GCP. */ + UNKNOWN_PLATFORM, + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleAppEngine.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleAppEngine.java new file mode 100644 index 00000000..64335263 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleAppEngine.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GAE_APP_VERSION; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GAE_AVAILABILITY_ZONE; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GAE_CLOUD_REGION; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GAE_INSTANCE_ID; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GAE_MODULE_NAME; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +final class GoogleAppEngine implements DetectedPlatform { + private final EnvironmentVariables environmentVariables; + private final GCPMetadataConfig metadataConfig; + private final Map availableAttributes; + + GoogleAppEngine(EnvironmentVariables environmentVariables, GCPMetadataConfig metadataConfig) { + this.environmentVariables = environmentVariables; + this.metadataConfig = metadataConfig; + this.availableAttributes = prepareAttributes(); + } + + private Map prepareAttributes() { + Map map = new HashMap<>(); + map.put(GAE_MODULE_NAME, this.environmentVariables.get("GAE_SERVICE")); + map.put(GAE_APP_VERSION, this.environmentVariables.get("GAE_VERSION")); + map.put(GAE_INSTANCE_ID, this.environmentVariables.get("GAE_INSTANCE")); + map.put(GAE_AVAILABILITY_ZONE, this.metadataConfig.getZone()); + map.put(GAE_CLOUD_REGION, getCloudRegion()); + return Collections.unmodifiableMap(map); + } + + private String getCloudRegion() { + if (this.environmentVariables.get("GAE_ENV") != null + && this.environmentVariables.get("GAE_ENV").equals("standard")) { + return this.metadataConfig.getRegion(); + } else { + return this.metadataConfig.getRegionFromZone(); + } + } + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.GOOGLE_APP_ENGINE; + } + + @Override + public String getProjectId() { + return this.metadataConfig.getProjectId(); + } + + @Override + public Map getAttributes() { + return this.availableAttributes; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudFunction.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudFunction.java new file mode 100644 index 00000000..e31df554 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudFunction.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +class GoogleCloudFunction extends GoogleServerlessCompute { + GoogleCloudFunction(EnvironmentVariables environmentVariables, GCPMetadataConfig metadataConfig) { + super(environmentVariables, metadataConfig); + } + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRun.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRun.java new file mode 100644 index 00000000..e7988366 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleCloudRun.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +class GoogleCloudRun extends GoogleServerlessCompute { + GoogleCloudRun(EnvironmentVariables environmentVariables, GCPMetadataConfig metadataConfig) { + super(environmentVariables, metadataConfig); + } + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleComputeEngine.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleComputeEngine.java new file mode 100644 index 00000000..18bceaef --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleComputeEngine.java @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_AVAILABILITY_ZONE; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_CLOUD_REGION; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_INSTANCE_HOSTNAME; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_INSTANCE_ID; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_INSTANCE_NAME; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GCE_MACHINE_TYPE; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +final class GoogleComputeEngine implements DetectedPlatform { + private final GCPMetadataConfig metadataConfig; + private final Map availableAttributes; + + GoogleComputeEngine(GCPMetadataConfig metadataConfig) { + this.metadataConfig = metadataConfig; + this.availableAttributes = prepareAttributes(); + } + + private Map prepareAttributes() { + Map map = new HashMap<>(); + map.put(GCE_AVAILABILITY_ZONE, this.metadataConfig.getZone()); + map.put(GCE_CLOUD_REGION, this.metadataConfig.getRegionFromZone()); + map.put(GCE_INSTANCE_ID, this.metadataConfig.getInstanceId()); + map.put(GCE_INSTANCE_NAME, this.metadataConfig.getInstanceName()); + map.put(GCE_INSTANCE_HOSTNAME, this.metadataConfig.getInstanceHostName()); + map.put(GCE_MACHINE_TYPE, this.metadataConfig.getMachineType()); + return Collections.unmodifiableMap(map); + } + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.GOOGLE_COMPUTE_ENGINE; + } + + @Override + public String getProjectId() { + return this.metadataConfig.getProjectId(); + } + + @Override + public Map getAttributes() { + return this.availableAttributes; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleKubernetesEngine.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleKubernetesEngine.java new file mode 100644 index 00000000..c632623b --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleKubernetesEngine.java @@ -0,0 +1,75 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_CLUSTER_LOCATION; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_CLUSTER_LOCATION_TYPE; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_CLUSTER_NAME; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_HOST_ID; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_LOCATION_TYPE_REGION; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.GKE_LOCATION_TYPE_ZONE; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +final class GoogleKubernetesEngine implements DetectedPlatform { + private final GCPMetadataConfig metadataConfig; + private final Map availableAttributes; + + GoogleKubernetesEngine(GCPMetadataConfig metadataConfig) { + this.metadataConfig = metadataConfig; + this.availableAttributes = prepareAttributes(); + } + + private Map prepareAttributes() { + Map map = new HashMap<>(); + map.put(GKE_CLUSTER_NAME, this.metadataConfig.getClusterName()); + map.put(GKE_CLUSTER_LOCATION, this.metadataConfig.getClusterLocation()); + map.put(GKE_CLUSTER_LOCATION_TYPE, this.getClusterLocationType()); + map.put(GKE_HOST_ID, this.metadataConfig.getInstanceId()); + return Collections.unmodifiableMap(map); + } + + private String getClusterLocationType() { + String clusterLocation = this.metadataConfig.getClusterLocation(); + long dashCount = + (clusterLocation == null || clusterLocation.isEmpty()) + ? 0 + : clusterLocation.chars().filter(ch -> ch == '-').count(); + if (dashCount == 1) { + return GKE_LOCATION_TYPE_REGION; + } else if (dashCount == 2) { + return GKE_LOCATION_TYPE_ZONE; + } + return ""; + } + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE; + } + + @Override + public String getProjectId() { + return this.metadataConfig.getProjectId(); + } + + @Override + public Map getAttributes() { + return this.availableAttributes; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleServerlessCompute.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleServerlessCompute.java new file mode 100644 index 00000000..b2fbc6f2 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/GoogleServerlessCompute.java @@ -0,0 +1,57 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * GoogleServerlessCompute adds attributes applicable to all serverless compute platforms in GCP. + * Currently, this includes Google Cloud Functions & Google Cloud Run. + */ +abstract class GoogleServerlessCompute implements DetectedPlatform { + private final EnvironmentVariables environmentVariables; + private final GCPMetadataConfig metadataConfig; + private final Map availableAttributes; + + GoogleServerlessCompute( + EnvironmentVariables environmentVariables, GCPMetadataConfig metadataConfig) { + this.environmentVariables = environmentVariables; + this.metadataConfig = metadataConfig; + this.availableAttributes = prepareAttributes(); + } + + private Map prepareAttributes() { + Map map = new HashMap<>(); + map.put(AttributeKeys.SERVERLESS_COMPUTE_NAME, this.environmentVariables.get("K_SERVICE")); + map.put(AttributeKeys.SERVERLESS_COMPUTE_REVISION, this.environmentVariables.get("K_REVISION")); + map.put(AttributeKeys.SERVERLESS_COMPUTE_AVAILABILITY_ZONE, this.metadataConfig.getZone()); + map.put(AttributeKeys.SERVERLESS_COMPUTE_CLOUD_REGION, this.metadataConfig.getRegionFromZone()); + map.put(AttributeKeys.SERVERLESS_COMPUTE_INSTANCE_ID, this.metadataConfig.getInstanceId()); + return Collections.unmodifiableMap(map); + } + + @Override + public String getProjectId() { + return this.metadataConfig.getProjectId(); + } + + @Override + public Map getAttributes() { + return this.availableAttributes; + } +} diff --git a/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/UnknownPlatform.java b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/UnknownPlatform.java new file mode 100644 index 00000000..7de1bff5 --- /dev/null +++ b/detectors/resources-support/src/main/java/com/google/cloud/opentelemetry/detection/UnknownPlatform.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import java.util.Collections; +import java.util.Map; + +class UnknownPlatform implements DetectedPlatform { + + UnknownPlatform() {} + + @Override + public GCPPlatformDetector.SupportedPlatform getSupportedPlatform() { + return GCPPlatformDetector.SupportedPlatform.UNKNOWN_PLATFORM; + } + + @Override + public String getProjectId() { + return ""; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } +} diff --git a/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/EnvVarMock.java b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/EnvVarMock.java similarity index 85% rename from detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/EnvVarMock.java rename to detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/EnvVarMock.java index aa4ec025..2806948a 100644 --- a/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/EnvVarMock.java +++ b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/EnvVarMock.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.cloud.opentelemetry.detectors; +package com.google.cloud.opentelemetry.detection; import java.util.Map; -class EnvVarMock implements EnvVars { +class EnvVarMock implements EnvironmentVariables { private final Map mock; public EnvVarMock(Map mock) { diff --git a/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfigTest.java b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfigTest.java new file mode 100644 index 00000000..4d1f3ab7 --- /dev/null +++ b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPMetadataConfigTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.stream.Stream; +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; + +@WireMockTest(httpPort = 8090) +class GCPMetadataConfigTest { + private static final String mockProjectId = "pid"; + private static final String mockZone = "country-region-zone"; + private static final String mockRegion = "country-region1"; + private static final String mockInstanceId = "instance-id"; + private static final String mockInstanceName = "instance-name"; + private static final String mockInstanceType = "instance-type"; + private static final String mockClusterName = "cluster-name"; + private static final String mockClusterLocation = "cluster-location"; + private static final String mockHostname = "hostname"; + + private final GCPMetadataConfig mockMetadataConfig = + new GCPMetadataConfig("http://localhost:8090/"); + + @BeforeEach + public void setupMockMetadataConfig() { + TestUtils.stubEndpoint("/project/project-id", mockProjectId); + TestUtils.stubEndpoint("/instance/zone", mockZone); + TestUtils.stubEndpoint("/instance/region", mockRegion); + TestUtils.stubEndpoint("/instance/id", mockInstanceId); + TestUtils.stubEndpoint("/instance/name", mockInstanceName); + TestUtils.stubEndpoint("/instance/machine-type", mockInstanceType); + TestUtils.stubEndpoint("/instance/attributes/cluster-name", mockClusterName); + TestUtils.stubEndpoint("/instance/attributes/cluster-location", mockClusterLocation); + TestUtils.stubEndpoint("/instance/hostname", mockHostname); + } + + @Test + void testGetProjectId() { + assertEquals(mockProjectId, mockMetadataConfig.getProjectId()); + } + + /** Test Zone Retrieval */ + @ParameterizedTest + @MethodSource("provideZoneRetrievalArguments") + void testGetZone(String stubbedMockZone, String expectedMockZone) { + TestUtils.stubEndpoint("/instance/zone", stubbedMockZone); + assertEquals(expectedMockZone, mockMetadataConfig.getZone()); + } + + private static Stream provideZoneRetrievalArguments() { + return Stream.of( + Arguments.of(mockZone, mockZone), + Arguments.of( + "projects/640212054955/zones/australia-southeast1-a", "australia-southeast1-a"), + Arguments.of("", null), + Arguments.of(null, null)); + } + + /** Test Region Retrieval */ + @ParameterizedTest + @MethodSource("provideRegionRetrievalArguments") + void testGetRegion(String stubbedMockRegion, String expectedMockRegion) { + TestUtils.stubEndpoint("/instance/region", stubbedMockRegion); + assertEquals(expectedMockRegion, mockMetadataConfig.getRegion()); + } + + private static Stream provideRegionRetrievalArguments() { + return Stream.of( + Arguments.of(mockRegion, mockRegion), + Arguments.of("projects/640212054955/regions/us-central1", "us-central1"), + Arguments.of("", null), + Arguments.of(null, null)); + } + + /** Test Region Retrieval from Zone */ + @ParameterizedTest + @MethodSource("provideZoneArguments") + void testGetRegionFromZone(String stubbedMockZone, String expectedRegion) { + TestUtils.stubEndpoint("/instance/zone", stubbedMockZone); + assertEquals(expectedRegion, mockMetadataConfig.getRegionFromZone()); + } + + private static Stream provideZoneArguments() { + return Stream.of( + Arguments.of(mockZone, "country-region"), + Arguments.of("projects/640212054955/zones/australia-southeast1-a", "australia-southeast1"), + Arguments.of("country-region", null), + Arguments.of("", null), + Arguments.of(null, null)); + } + + /** Test Machine Type Retrieval */ + @ParameterizedTest + @MethodSource("provideMachineTypeRetrievalArguments") + void testGetMachineType(String stubbedMockMachineType, String expectedMockMachineType) { + TestUtils.stubEndpoint("/instance/machine-type", stubbedMockMachineType); + assertEquals(expectedMockMachineType, mockMetadataConfig.getMachineType()); + } + + private static Stream provideMachineTypeRetrievalArguments() { + return Stream.of( + Arguments.of(mockInstanceType, mockInstanceType), + Arguments.of("projects/640212054955/machineTypes/e2-medium", "e2-medium"), + Arguments.of("", null), + Arguments.of(null, null)); + } + + @Test + void testGetInstanceId() { + assertEquals(mockInstanceId, mockMetadataConfig.getInstanceId()); + } + + @Test + void testGetClusterName() { + assertEquals(mockClusterName, mockMetadataConfig.getClusterName()); + } + + @Test + void testGetClusterLocation() { + assertEquals(mockClusterLocation, mockMetadataConfig.getClusterLocation()); + } + + @Test + void testGetInstanceHostName() { + assertEquals(mockHostname, mockMetadataConfig.getInstanceHostName()); + } + + @Test + void testGetInstanceName() { + assertEquals(mockInstanceName, mockMetadataConfig.getInstanceName()); + } +} diff --git a/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java new file mode 100644 index 00000000..47f29675 --- /dev/null +++ b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/GCPPlatformDetectorTest.java @@ -0,0 +1,358 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +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.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; + +@WireMockTest(httpPort = 8089) +public class GCPPlatformDetectorTest { + private final GCPMetadataConfig mockMetadataConfig = + new GCPMetadataConfig("http://localhost:8089/"); + private static final Map envVars = new HashMap<>(); + + @BeforeEach + public void setup() { + envVars.clear(); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {""}) + public void testGCPComputeResourceNotGCP(String projectId) { + GCPMetadataConfig mockMetadataConfig = Mockito.mock(GCPMetadataConfig.class); + Mockito.when(mockMetadataConfig.getProjectId()).thenReturn(projectId); + + GCPPlatformDetector detector = + new GCPPlatformDetector(mockMetadataConfig, EnvironmentVariables.DEFAULT_INSTANCE); + // If GCPMetadataConfig cannot find ProjectId, then the platform should be unsupported + assertEquals( + GCPPlatformDetector.SupportedPlatform.UNKNOWN_PLATFORM, + detector.detectPlatform().getSupportedPlatform()); + assertEquals(Collections.emptyMap(), detector.detectPlatform().getAttributes()); + } + + @Test + public void testGCPComputeResourceNonGCPEndpoint() { + // intentionally not providing the required Metadata-Flavor header with the + // request to mimic non GCP endpoint + stubFor( + get(urlEqualTo("/project/project-id")) + .willReturn(aResponse().withBody("nonGCPEndpointTest"))); + GCPPlatformDetector detector = + new GCPPlatformDetector(mockMetadataConfig, EnvironmentVariables.DEFAULT_INSTANCE); + assertEquals( + GCPPlatformDetector.SupportedPlatform.UNKNOWN_PLATFORM, + detector.detectPlatform().getSupportedPlatform()); + assertEquals(Collections.emptyMap(), detector.detectPlatform().getAttributes()); + } + + /** Google Compute Engine Tests * */ + @Test + public void testGCEResourceWithGCEAttributesSucceeds() { + TestUtils.stubEndpoint("/project/project-id", "GCE-pid"); + TestUtils.stubEndpoint("/instance/zone", "country-gce_region-gce_zone"); + TestUtils.stubEndpoint("/instance/id", "GCE-instance-id"); + TestUtils.stubEndpoint("/instance/name", "GCE-instance-name"); + TestUtils.stubEndpoint("/instance/machine-type", "GCE-instance-type"); + TestUtils.stubEndpoint("/instance/hostname", "GCE-instance-hostname"); + + GCPPlatformDetector detector = + new GCPPlatformDetector(mockMetadataConfig, new EnvVarMock(envVars)); + + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_COMPUTE_ENGINE, + detector.detectPlatform().getSupportedPlatform()); + assertEquals("GCE-pid", detector.detectPlatform().getProjectId()); + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals(new GoogleComputeEngine(mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals(6, detectedAttributes.size()); + + assertEquals("country-gce_region-gce_zone", detectedAttributes.get(GCE_AVAILABILITY_ZONE)); + assertEquals("country-gce_region", detectedAttributes.get(GCE_CLOUD_REGION)); + assertEquals("GCE-instance-id", detectedAttributes.get(GCE_INSTANCE_ID)); + assertEquals("GCE-instance-name", detectedAttributes.get(GCE_INSTANCE_NAME)); + assertEquals("GCE-instance-type", detectedAttributes.get(GCE_MACHINE_TYPE)); + assertEquals("GCE-instance-hostname", detectedAttributes.get(GCE_INSTANCE_HOSTNAME)); + } + + /** Google Kubernetes Engine Tests * */ + @Test + public void testGKEResourceWithGKEAttributesSucceedsLocationZone() { + envVars.put("KUBERNETES_SERVICE_HOST", "GKE-testHost"); + envVars.put("NAMESPACE", "GKE-testNameSpace"); + // Hostname can truncate pod name, so we test downward API override. + envVars.put("HOSTNAME", "GKE-testHostName"); + envVars.put("POD_NAME", "GKE-testHostName-full-1234"); + envVars.put("CONTAINER_NAME", "GKE-testContainerName"); + + TestUtils.stubEndpoint("/project/project-id", "GKE-pid"); + TestUtils.stubEndpoint("/instance/id", "GKE-instance-id"); + TestUtils.stubEndpoint("/instance/name", "instance-name"); + TestUtils.stubEndpoint("/instance/machine-type", "instance-type"); + TestUtils.stubEndpoint("/instance/attributes/cluster-name", "GKE-cluster-name"); + TestUtils.stubEndpoint("/instance/attributes/cluster-location", "country-region-zone"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleKubernetesEngine(mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GKE-pid", detector.detectPlatform().getProjectId()); + assertEquals(4, detectedAttributes.size()); + + assertEquals(GKE_LOCATION_TYPE_ZONE, detectedAttributes.get(GKE_CLUSTER_LOCATION_TYPE)); + assertEquals("country-region-zone", detectedAttributes.get(GKE_CLUSTER_LOCATION)); + assertEquals("GKE-cluster-name", detectedAttributes.get(GKE_CLUSTER_NAME)); + assertEquals("GKE-instance-id", detectedAttributes.get(GKE_HOST_ID)); + } + + @Test + public void testGKEResourceWithGKEAttributesSucceedsLocationRegion() { + envVars.put("KUBERNETES_SERVICE_HOST", "GKE-testHost"); + envVars.put("NAMESPACE", "GKE-testNameSpace"); + // Hostname can truncate pod name, so we test downward API override. + envVars.put("HOSTNAME", "GKE-testHostName"); + envVars.put("POD_NAME", "GKE-testHostName-full-1234"); + envVars.put("CONTAINER_NAME", "GKE-testContainerName"); + + TestUtils.stubEndpoint("/project/project-id", "GKE-pid"); + TestUtils.stubEndpoint("/instance/id", "GKE-instance-id"); + TestUtils.stubEndpoint("/instance/name", "GCE-instance-name"); + TestUtils.stubEndpoint("/instance/machine-type", "GKE-instance-type"); + TestUtils.stubEndpoint("/instance/attributes/cluster-name", "GKE-cluster-name"); + TestUtils.stubEndpoint("/instance/attributes/cluster-location", "country-region"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleKubernetesEngine(mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GKE-pid", detector.detectPlatform().getProjectId()); + assertEquals(4, detectedAttributes.size()); + + assertEquals(GKE_LOCATION_TYPE_REGION, detectedAttributes.get(GKE_CLUSTER_LOCATION_TYPE)); + assertEquals("country-region", detectedAttributes.get(GKE_CLUSTER_LOCATION)); + assertEquals("GKE-cluster-name", detectedAttributes.get(GKE_CLUSTER_NAME)); + assertEquals("GKE-instance-id", detectedAttributes.get(GKE_HOST_ID)); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "country", "country-region-zone-invalid"}) + public void testGKEResourceDetectionWithInvalidLocations(String clusterLocation) { + envVars.put("KUBERNETES_SERVICE_HOST", "GKE-testHost"); + envVars.put("NAMESPACE", "GKE-testNameSpace"); + // Hostname can truncate pod name, so we test downward API override. + envVars.put("HOSTNAME", "GKE-testHostName"); + envVars.put("POD_NAME", "GKE-testHostName-full-1234"); + envVars.put("CONTAINER_NAME", "GKE-testContainerName"); + + TestUtils.stubEndpoint("/project/project-id", "GKE-pid"); + TestUtils.stubEndpoint("/instance/id", "GKE-instance-id"); + TestUtils.stubEndpoint("/instance/name", "GKE-instance-name"); + TestUtils.stubEndpoint("/instance/machine-type", "GKE-instance-type"); + TestUtils.stubEndpoint("/instance/attributes/cluster-name", "GKE-cluster-name"); + TestUtils.stubEndpoint("/instance/attributes/cluster-location", clusterLocation); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleKubernetesEngine(mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GKE-pid", detector.detectPlatform().getProjectId()); + assertEquals(4, detectedAttributes.size()); + + assertEquals("", detector.detectPlatform().getAttributes().get(GKE_CLUSTER_LOCATION_TYPE)); + if (clusterLocation == null || clusterLocation.isEmpty()) { + assertNull(detectedAttributes.get(GKE_CLUSTER_LOCATION)); + } else { + assertEquals(clusterLocation, detectedAttributes.get(GKE_CLUSTER_LOCATION)); + } + assertEquals("GKE-cluster-name", detectedAttributes.get(GKE_CLUSTER_NAME)); + assertEquals("GKE-instance-id", detectedAttributes.get(GKE_HOST_ID)); + } + + /** Google Cloud Functions Tests * */ + @Test + public void testGCFResourceWithCloudFunctionAttributesSucceeds() { + // Setup GCF required env vars + envVars.put("K_SERVICE", "cloud-function-hello"); + envVars.put("K_REVISION", "cloud-function-hello.1"); + envVars.put("FUNCTION_TARGET", "cloud-function-hello"); + + TestUtils.stubEndpoint("/project/project-id", "GCF-pid"); + TestUtils.stubEndpoint("/instance/zone", "country-region-zone"); + TestUtils.stubEndpoint("/instance/id", "GCF-instance-id"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleCloudFunction(mockEnv, mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GCF-pid", detector.detectPlatform().getProjectId()); + assertEquals(5, detectedAttributes.size()); + + assertEquals("cloud-function-hello", detectedAttributes.get(SERVERLESS_COMPUTE_NAME)); + assertEquals("cloud-function-hello.1", detectedAttributes.get(SERVERLESS_COMPUTE_REVISION)); + assertEquals( + "country-region-zone", detectedAttributes.get(SERVERLESS_COMPUTE_AVAILABILITY_ZONE)); + assertEquals("country-region", detectedAttributes.get(SERVERLESS_COMPUTE_CLOUD_REGION)); + assertEquals("GCF-instance-id", detectedAttributes.get(SERVERLESS_COMPUTE_INSTANCE_ID)); + } + + @Test + public void testGCFDetectionWhenGCRAttributesPresent() { + // Setup GCF required env vars + envVars.put("K_SERVICE", "cloud-function-hello"); + envVars.put("K_REVISION", "cloud-function-hello.1"); + envVars.put("FUNCTION_TARGET", "cloud-function-hello"); + // This should be ignored and detected platform should still be GCF + envVars.put("K_CONFIGURATION", "cloud-run-hello"); + + TestUtils.stubEndpoint("/project/project-id", "GCF-pid"); + TestUtils.stubEndpoint("/instance/zone", "country-region-zone"); + TestUtils.stubEndpoint("/instance/id", "GCF-instance-id"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS, + detector.detectPlatform().getSupportedPlatform()); + assertEquals("GCF-pid", detector.detectPlatform().getProjectId()); + assertEquals( + new GoogleCloudFunction(mockEnv, mockMetadataConfig).getAttributes(), + detector.detectPlatform().getAttributes()); + } + + /** Google Cloud Run Tests * */ + @Test + public void testGCFResourceWithCloudRunAttributesSucceeds() { + // Setup GCR required env vars + envVars.put("K_SERVICE", "cloud-run-hello"); + envVars.put("K_REVISION", "cloud-run-hello.1"); + envVars.put("K_CONFIGURATION", "cloud-run-hello"); + + TestUtils.stubEndpoint("/project/project-id", "GCR-pid"); + TestUtils.stubEndpoint("/instance/zone", "country-region-zone"); + TestUtils.stubEndpoint("/instance/id", "GCR-instance-id"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleCloudFunction(mockEnv, mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GCR-pid", detector.detectPlatform().getProjectId()); + assertEquals(5, detectedAttributes.size()); + + assertEquals("cloud-run-hello", detectedAttributes.get(SERVERLESS_COMPUTE_NAME)); + assertEquals("cloud-run-hello.1", detectedAttributes.get(SERVERLESS_COMPUTE_REVISION)); + assertEquals( + "country-region-zone", detectedAttributes.get(SERVERLESS_COMPUTE_AVAILABILITY_ZONE)); + assertEquals("country-region", detectedAttributes.get(SERVERLESS_COMPUTE_CLOUD_REGION)); + assertEquals("GCR-instance-id", detectedAttributes.get(SERVERLESS_COMPUTE_INSTANCE_ID)); + } + + /** Google App Engine Tests * */ + @ParameterizedTest + @MethodSource("provideGAEVariantEnvironmentVariable") + public void testGAEResourceWithAppEngineAttributesSucceeds(String gaeEnvironmentVar) { + envVars.put("GAE_SERVICE", "app-engine-hello"); + envVars.put("GAE_VERSION", "app-engine-hello-v1"); + envVars.put("GAE_INSTANCE", "app-engine-hello-f236d"); + envVars.put("GAE_ENV", gaeEnvironmentVar); + + TestUtils.stubEndpoint("/project/project-id", "GAE-pid"); + // for standard, the region should be extracted from region attribute + TestUtils.stubEndpoint("/instance/zone", "country-region-zone"); + TestUtils.stubEndpoint("/instance/region", "country-region1"); + TestUtils.stubEndpoint("/instance/id", "GAE-instance-id"); + + EnvironmentVariables mockEnv = new EnvVarMock(envVars); + GCPPlatformDetector detector = new GCPPlatformDetector(mockMetadataConfig, mockEnv); + + Map detectedAttributes = detector.detectPlatform().getAttributes(); + assertEquals( + GCPPlatformDetector.SupportedPlatform.GOOGLE_APP_ENGINE, + detector.detectPlatform().getSupportedPlatform()); + assertEquals( + new GoogleAppEngine(mockEnv, mockMetadataConfig).getAttributes(), detectedAttributes); + assertEquals("GAE-pid", detector.detectPlatform().getProjectId()); + assertEquals(5, detectedAttributes.size()); + + if (gaeEnvironmentVar != null && gaeEnvironmentVar.equals("standard")) { + assertEquals( + "country-region1", detector.detectPlatform().getAttributes().get(GAE_CLOUD_REGION)); + } else { + assertEquals( + "country-region", detector.detectPlatform().getAttributes().get(GAE_CLOUD_REGION)); + } + assertEquals("app-engine-hello", detectedAttributes.get(GAE_MODULE_NAME)); + assertEquals("app-engine-hello-v1", detectedAttributes.get(GAE_APP_VERSION)); + assertEquals("app-engine-hello-f236d", detectedAttributes.get(GAE_INSTANCE_ID)); + assertEquals("country-region-zone", detectedAttributes.get(GAE_AVAILABILITY_ZONE)); + } + + // Provides key-value pair of GAE variant environment and the expected region + // value based on the environment variable + private static Stream provideGAEVariantEnvironmentVariable() { + return Stream.of( + Arguments.of("standard"), + Arguments.of((String) null), + Arguments.of("flex"), + Arguments.of("")); + } +} diff --git a/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/TestUtils.java b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/TestUtils.java new file mode 100644 index 00000000..bef3a53c --- /dev/null +++ b/detectors/resources-support/src/test/java/com/google/cloud/opentelemetry/detection/TestUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed 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 com.google.cloud.opentelemetry.detection; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; + +public final class TestUtils { + // Helper method to help stub endpoints + public static void stubEndpoint(String endpointPath, String responseBody) { + stubFor( + get(urlEqualTo(endpointPath)) + .willReturn( + aResponse().withHeader("Metadata-Flavor", "Google").withBody(responseBody))); + } +} diff --git a/detectors/resources/build.gradle b/detectors/resources/build.gradle index 2d086614..7c102352 100644 --- a/detectors/resources/build.gradle +++ b/detectors/resources/build.gradle @@ -27,9 +27,13 @@ dependencies { implementation(libraries.opentelemetry_sdk_autoconf) implementation(libraries.opentelemetry_semconv) implementation platform(libraries.opentelemetry_bom) - testImplementation(testLibraries.assertj) - testImplementation(testLibraries.junit) - testImplementation(testLibraries.wiremock) + implementation project(':detector-resources-support') + testImplementation(testLibraries.junit5) + testRuntimeOnly(testLibraries.junit5_runtime) testImplementation(testLibraries.mockito) - testImplementation(testLibraries.opentelemetry_sdk_testing) +} + +test { + // required for discovering JUnit 5 tests + useJUnitPlatform() } diff --git a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/AttributesExtractorUtil.java b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/AttributesExtractorUtil.java index 8c05cbc5..e57058a2 100644 --- a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/AttributesExtractorUtil.java +++ b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/AttributesExtractorUtil.java @@ -24,7 +24,10 @@ * *

This class only adds helper methods to extract {@link ResourceAttributes} that are common * across all the supported compute environments. + * + * @deprecated Not for public use. This class is expected to be retained only as package private. */ +@Deprecated public class AttributesExtractorUtil { /** diff --git a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/EnvVars.java b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/EnvVars.java index ea665670..e34f87da 100644 --- a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/EnvVars.java +++ b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/EnvVars.java @@ -18,7 +18,11 @@ /** * Provides API to fetch environment variables. This is useful in order to create a mock class for * testing. + * + * @deprecated Not for public use. This interface is expected to be retained only as package + * private. */ +@Deprecated public interface EnvVars { EnvVars DEFAULT_INSTANCE = System::getenv; diff --git a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPMetadataConfig.java b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPMetadataConfig.java index ab57dcf8..53f083ff 100644 --- a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPMetadataConfig.java +++ b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPMetadataConfig.java @@ -30,7 +30,10 @@ * * @see * https://cloud.google.com/compute/docs/storing-retrieving-metadata + * @deprecated This class is no longer used, it is only maintained here to support {@link + * AttributesExtractorUtil}. */ +@Deprecated final class GCPMetadataConfig { private static final String DEFAULT_URL = "http://metadata.google.internal/computeMetadata/v1/"; public static final GCPMetadataConfig DEFAULT_INSTANCE = new GCPMetadataConfig(DEFAULT_URL); diff --git a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPResource.java b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPResource.java index d56ca208..decd5d75 100644 --- a/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPResource.java +++ b/detectors/resources/src/main/java/com/google/cloud/opentelemetry/detectors/GCPResource.java @@ -15,13 +15,18 @@ */ package com.google.cloud.opentelemetry.detectors; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.*; + +import com.google.cloud.opentelemetry.detection.DetectedPlatform; +import com.google.cloud.opentelemetry.detection.GCPPlatformDetector; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; -import io.opentelemetry.api.internal.StringUtils; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.semconv.ResourceAttributes; +import java.util.Map; +import java.util.Optional; import java.util.logging.Logger; /** @@ -30,20 +35,17 @@ * App Engine (GAE) and Google Cloud Run (GCR). */ public class GCPResource implements ResourceProvider { - private final GCPMetadataConfig metadata; - private final EnvVars envVars; private static final Logger LOGGER = Logger.getLogger(GCPResource.class.getSimpleName()); + private final GCPPlatformDetector detector; - public GCPResource() { - this.metadata = GCPMetadataConfig.DEFAULT_INSTANCE; - this.envVars = EnvVars.DEFAULT_INSTANCE; + // for testing only + GCPResource(GCPPlatformDetector detector) { + this.detector = detector; } - // for testing only - GCPResource(GCPMetadataConfig metadata, EnvVars envVars) { - this.metadata = metadata; - this.envVars = envVars; + public GCPResource() { + this.detector = GCPPlatformDetector.DEFAULT_INSTANCE; } /** @@ -53,20 +55,35 @@ public GCPResource() { * @return The {@link Attributes} for the detected resource. */ public Attributes getAttributes() { - if (!metadata.isRunningOnGcp()) { + DetectedPlatform detectedPlatform = detector.detectPlatform(); + if (detectedPlatform.getSupportedPlatform() + == GCPPlatformDetector.SupportedPlatform.UNKNOWN_PLATFORM) { return Attributes.empty(); } // This is running on some sort of GCPCompute - figure out the platform AttributesBuilder attrBuilder = Attributes.builder(); attrBuilder.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP); + attrBuilder.put(ResourceAttributes.CLOUD_ACCOUNT_ID, detectedPlatform.getProjectId()); - if (!(generateGKEAttributesIfApplicable(attrBuilder) - || generateGCRAttributesIfApplicable(attrBuilder) - || generateGCFAttributesIfApplicable(attrBuilder) - || generateGAEAttributesIfApplicable(attrBuilder))) { - // none of the above GCP platforms is applicable, default to GCE - addGCEAttributes(attrBuilder); + switch (detectedPlatform.getSupportedPlatform()) { + case GOOGLE_KUBERNETES_ENGINE: + addGKEAttributes(attrBuilder, detectedPlatform.getAttributes()); + break; + case GOOGLE_CLOUD_RUN: + addGCRAttributes(attrBuilder, detectedPlatform.getAttributes()); + break; + case GOOGLE_CLOUD_FUNCTIONS: + addGCFAttributes(attrBuilder, detectedPlatform.getAttributes()); + break; + case GOOGLE_APP_ENGINE: + addGAEAttributes(attrBuilder, detectedPlatform.getAttributes()); + break; + case GOOGLE_COMPUTE_ENGINE: + addGCEAttributes(attrBuilder, detectedPlatform.getAttributes()); + break; + default: + // We don't support this platform yet, so just return with what we have } return attrBuilder.build(); @@ -83,33 +100,29 @@ public Resource createResource(ConfigProperties config) { * additional attributes are added/overwritten if later on, the resource is identified to be some * other platform - like GKE, GAE, etc. */ - private void addGCEAttributes(AttributesBuilder attrBuilder) { + private void addGCEAttributes(AttributesBuilder attrBuilder, Map attributesMap) { attrBuilder.put( ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE); - String projectId = metadata.getProjectId(); - if (projectId != null) { - attrBuilder.put(ResourceAttributes.CLOUD_ACCOUNT_ID, projectId); - } - - AttributesExtractorUtil.addAvailabilityZoneFromMetadata(attrBuilder, metadata); - AttributesExtractorUtil.addCloudRegionFromMetadataUsingZone(attrBuilder, metadata); - - String instanceId = metadata.getInstanceId(); - if (instanceId != null) { - attrBuilder.put(ResourceAttributes.HOST_ID, instanceId); - } - - String instanceName = metadata.getInstanceName(); - if (instanceName != null) { - attrBuilder.put(ResourceAttributes.HOST_NAME, instanceName); - } - - String hostType = metadata.getMachineType(); - if (hostType != null) { - attrBuilder.put(ResourceAttributes.HOST_TYPE, hostType); - } + Optional.ofNullable(attributesMap.get(GCE_AVAILABILITY_ZONE)) + .ifPresent(zone -> attrBuilder.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, zone)); + Optional.ofNullable(attributesMap.get(GCE_CLOUD_REGION)) + .ifPresent(region -> attrBuilder.put(ResourceAttributes.CLOUD_REGION, region)); + Optional.ofNullable(attributesMap.get(GCE_INSTANCE_ID)) + .ifPresent(instanceId -> attrBuilder.put(ResourceAttributes.HOST_ID, instanceId)); + Optional.ofNullable(attributesMap.get(GCE_INSTANCE_NAME)) + .ifPresent( + instanceName -> { + attrBuilder.put(ResourceAttributes.HOST_NAME, instanceName); + attrBuilder.put(ResourceAttributes.GCP_GCE_INSTANCE_NAME, instanceName); + }); + Optional.ofNullable(attributesMap.get(GCE_INSTANCE_HOSTNAME)) + .ifPresent( + instanceHostname -> + attrBuilder.put(ResourceAttributes.GCP_GCE_INSTANCE_HOSTNAME, instanceHostname)); + Optional.ofNullable(attributesMap.get(GCE_MACHINE_TYPE)) + .ifPresent(machineType -> attrBuilder.put(ResourceAttributes.HOST_TYPE, machineType)); } /** @@ -118,79 +131,39 @@ private void addGCEAttributes(AttributesBuilder attrBuilder) { * * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the * necessary keys. - * @return a boolean indicating if the environment was determined to be GKE and GKE specific - * attributes were applied. */ - private boolean generateGKEAttributesIfApplicable(AttributesBuilder attrBuilder) { - if (envVars.get("KUBERNETES_SERVICE_HOST") != null) { - attrBuilder.put( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE); - String podName = envVars.get("POD_NAME"); - if (podName != null && !podName.isEmpty()) { - attrBuilder.put(ResourceAttributes.K8S_POD_NAME, podName); - } else { - // If nothing else is set, at least use hostname for pod name. - attrBuilder.put(ResourceAttributes.K8S_POD_NAME, envVars.get("HOSTNAME")); - } - - String namespace = envVars.get("NAMESPACE"); - if (namespace != null && !namespace.isEmpty()) { - attrBuilder.put(ResourceAttributes.K8S_NAMESPACE_NAME, namespace); - } - - String containerName = envVars.get("CONTAINER_NAME"); - if (containerName != null && !containerName.isEmpty()) { - attrBuilder.put(ResourceAttributes.K8S_CONTAINER_NAME, containerName); - } - - String instanceId = metadata.getInstanceId(); - if (instanceId != null) { - attrBuilder.put(ResourceAttributes.HOST_ID, instanceId); - } - - String clusterLocation = metadata.getClusterLocation(); - assignGKEAvailabilityZoneOrRegion(clusterLocation, attrBuilder); - - String clusterName = metadata.getClusterName(); - if (clusterName != null && !clusterName.isEmpty()) { - attrBuilder.put(ResourceAttributes.K8S_CLUSTER_NAME, clusterName); - } - return true; - } - return false; - } - - /** - * Function that assigns either the cloud region or cloud availability zone depending on whether - * the cluster is regional or zonal respectively. Assigns both values if the cluster location - * passed is in an unexpected format. - * - * @param clusterLocation The location of the GKE cluster. Can either be an availability zone or a - * region. - * @param attributesBuilder The {@link AttributesBuilder} object that needs to be updated with the - * necessary keys. - */ - private void assignGKEAvailabilityZoneOrRegion( - String clusterLocation, AttributesBuilder attributesBuilder) { - long dashCount = - StringUtils.isNullOrEmpty(clusterLocation) - ? 0 - : clusterLocation.chars().filter(ch -> ch == '-').count(); - switch ((int) dashCount) { - case 1: - // this is a region - attributesBuilder.put(ResourceAttributes.CLOUD_REGION, clusterLocation); - break; - case 2: - // this is a zone - attributesBuilder.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, clusterLocation); - break; - default: - // TODO: Figure out how to handle unexpected conditions like this - Issue #183 - LOGGER.severe( - String.format("Unrecognized format for cluster location: %s", clusterLocation)); - } + private void addGKEAttributes(AttributesBuilder attrBuilder, Map attributesMap) { + attrBuilder.put( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE); + + Optional.ofNullable(attributesMap.get(GKE_CLUSTER_NAME)) + .ifPresent( + clusterName -> attrBuilder.put(ResourceAttributes.K8S_CLUSTER_NAME, clusterName)); + Optional.ofNullable(attributesMap.get(GKE_HOST_ID)) + .ifPresent(hostId -> attrBuilder.put(ResourceAttributes.HOST_ID, hostId)); + Optional.ofNullable(attributesMap.get(GKE_CLUSTER_LOCATION_TYPE)) + .ifPresent( + locationType -> { + if (attributesMap.get(GKE_CLUSTER_LOCATION) != null) { + switch (locationType) { + case GKE_LOCATION_TYPE_REGION: + attrBuilder.put( + ResourceAttributes.CLOUD_REGION, attributesMap.get(GKE_CLUSTER_LOCATION)); + break; + case GKE_LOCATION_TYPE_ZONE: + attrBuilder.put( + ResourceAttributes.CLOUD_AVAILABILITY_ZONE, + attributesMap.get(GKE_CLUSTER_LOCATION)); + default: + // TODO: Figure out how to handle unexpected conditions like this - Issue #183 + LOGGER.severe( + String.format( + "Unrecognized format for cluster location: %s", + attributesMap.get(GKE_CLUSTER_LOCATION))); + } + } + }); } /** @@ -199,19 +172,11 @@ private void assignGKEAvailabilityZoneOrRegion( * * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the * necessary keys. - * @return a boolean indicating if the environment was determined to be GCR and GCR specific - * attributes were applied. */ - private boolean generateGCRAttributesIfApplicable(AttributesBuilder attrBuilder) { - if (envVars.get("K_CONFIGURATION") != null && envVars.get("FUNCTION_TARGET") == null) { - // add the resource attributes for Cloud Run - attrBuilder.put( - ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.GCP_CLOUD_RUN); - - updateCommonAttributesForServerlessCompute(attrBuilder); - return true; - } - return false; + private void addGCRAttributes(AttributesBuilder attrBuilder, Map attributesMap) { + attrBuilder.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.GCP_CLOUD_RUN); + addCommonAttributesForServerlessCompute(attrBuilder, attributesMap); } /** @@ -220,43 +185,12 @@ private boolean generateGCRAttributesIfApplicable(AttributesBuilder attrBuilder) * * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the * necessary keys. - * @return a boolean indicating if the environment was determined to be GCF and GCF specific - * attributes were applied. - */ - private boolean generateGCFAttributesIfApplicable(AttributesBuilder attrBuilder) { - if (envVars.get("FUNCTION_TARGET") != null) { - // add the resource attributes for Cloud Function - attrBuilder.put( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_CLOUD_FUNCTIONS); - - updateCommonAttributesForServerlessCompute(attrBuilder); - return true; - } - return false; - } - - /** - * This function adds common attributes required for most serverless compute platforms within GCP. - * Currently, these attributes are required for both GCF and GCR. - * - * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the - * necessary keys. */ - private void updateCommonAttributesForServerlessCompute(AttributesBuilder attrBuilder) { - String serverlessComputeName = envVars.get("K_SERVICE"); - if (serverlessComputeName != null) { - attrBuilder.put(ResourceAttributes.FAAS_NAME, serverlessComputeName); - } - - String serverlessComputeVersion = envVars.get("K_REVISION"); - if (serverlessComputeVersion != null) { - attrBuilder.put(ResourceAttributes.FAAS_VERSION, serverlessComputeVersion); - } - - AttributesExtractorUtil.addAvailabilityZoneFromMetadata(attrBuilder, metadata); - AttributesExtractorUtil.addCloudRegionFromMetadataUsingZone(attrBuilder, metadata); - AttributesExtractorUtil.addInstanceIdFromMetadata(attrBuilder, metadata); + private void addGCFAttributes(AttributesBuilder attrBuilder, Map attributesMap) { + attrBuilder.put( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.GCP_CLOUD_FUNCTIONS); + addCommonAttributesForServerlessCompute(attrBuilder, attributesMap); } /** @@ -265,47 +199,43 @@ private void updateCommonAttributesForServerlessCompute(AttributesBuilder attrBu * * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the * necessary keys. - * @return a boolean indicating if the environment was determined to be GAE and GAE specific - * attributes were applied. */ - private boolean generateGAEAttributesIfApplicable(AttributesBuilder attrBuilder) { - if (envVars.get("GAE_SERVICE") != null) { - // add the resource attributes for App Engine - attrBuilder.put( - ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE); - - String appModuleName = envVars.get("GAE_SERVICE"); - if (appModuleName != null) { - attrBuilder.put(ResourceAttributes.FAAS_NAME, appModuleName); - } - - String appVersionId = envVars.get("GAE_VERSION"); - if (appVersionId != null) { - attrBuilder.put(ResourceAttributes.FAAS_VERSION, appVersionId); - } - - String appInstanceId = envVars.get("GAE_INSTANCE"); - if (appInstanceId != null) { - attrBuilder.put(ResourceAttributes.FAAS_INSTANCE, appInstanceId); - } - updateAttributesWithRegion(attrBuilder); - AttributesExtractorUtil.addAvailabilityZoneFromMetadata(attrBuilder, metadata); - return true; - } - return false; + private void addGAEAttributes(AttributesBuilder attrBuilder, Map attributesMap) { + attrBuilder.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE); + Optional.ofNullable(attributesMap.get(GAE_MODULE_NAME)) + .ifPresent(appName -> attrBuilder.put(ResourceAttributes.FAAS_NAME, appName)); + Optional.ofNullable(attributesMap.get(GAE_APP_VERSION)) + .ifPresent(appVersion -> attrBuilder.put(ResourceAttributes.FAAS_VERSION, appVersion)); + Optional.ofNullable(attributesMap.get(GAE_INSTANCE_ID)) + .ifPresent( + appInstanceId -> attrBuilder.put(ResourceAttributes.FAAS_INSTANCE, appInstanceId)); + Optional.ofNullable(attributesMap.get(GAE_CLOUD_REGION)) + .ifPresent(cloudRegion -> attrBuilder.put(ResourceAttributes.CLOUD_REGION, cloudRegion)); + Optional.ofNullable(attributesMap.get(GAE_AVAILABILITY_ZONE)) + .ifPresent( + cloudAvailabilityZone -> + attrBuilder.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, cloudAvailabilityZone)); } /** - * Selects the correct method to extract the region, depending on the GAE environment. + * This function adds common attributes required for most serverless compute platforms within GCP. + * Currently, these attributes are required for both GCF and GCR. * - * @param attributesBuilder The {@link AttributesBuilder} object to which the extracted region - * would be added. + * @param attrBuilder The {@link AttributesBuilder} object that needs to be updated with the + * necessary keys. */ - private void updateAttributesWithRegion(AttributesBuilder attributesBuilder) { - if (envVars.get("GAE_ENV") != null && envVars.get("GAE_ENV").equals("standard")) { - AttributesExtractorUtil.addCloudRegionFromMetadataUsingRegion(attributesBuilder, metadata); - } else { - AttributesExtractorUtil.addCloudRegionFromMetadataUsingZone(attributesBuilder, metadata); - } + private void addCommonAttributesForServerlessCompute( + AttributesBuilder attrBuilder, Map attributesMap) { + Optional.ofNullable(attributesMap.get(SERVERLESS_COMPUTE_NAME)) + .ifPresent(name -> attrBuilder.put(ResourceAttributes.FAAS_NAME, name)); + Optional.ofNullable(attributesMap.get(SERVERLESS_COMPUTE_REVISION)) + .ifPresent(revision -> attrBuilder.put(ResourceAttributes.FAAS_VERSION, revision)); + Optional.ofNullable(attributesMap.get(SERVERLESS_COMPUTE_INSTANCE_ID)) + .ifPresent(instanceId -> attrBuilder.put(ResourceAttributes.FAAS_INSTANCE, instanceId)); + Optional.ofNullable(attributesMap.get(SERVERLESS_COMPUTE_AVAILABILITY_ZONE)) + .ifPresent(zone -> attrBuilder.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, zone)); + Optional.ofNullable(attributesMap.get(SERVERLESS_COMPUTE_CLOUD_REGION)) + .ifPresent(region -> attrBuilder.put(ResourceAttributes.CLOUD_REGION, region)); } } diff --git a/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/GCPResourceTest.java b/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/GCPResourceTest.java index 84857095..88fb6369 100644 --- a/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/GCPResourceTest.java +++ b/detectors/resources/src/test/java/com/google/cloud/opentelemetry/detectors/GCPResourceTest.java @@ -15,240 +15,378 @@ */ package com.google.cloud.opentelemetry.detectors; -import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; -import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; -import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; -import static org.junit.Assert.assertTrue; - -import com.github.tomakehurst.wiremock.junit.WireMockRule; +import static com.google.cloud.opentelemetry.detection.AttributeKeys.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.cloud.opentelemetry.detection.DetectedPlatform; +import com.google.cloud.opentelemetry.detection.GCPPlatformDetector; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.semconv.ResourceAttributes; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.ServiceLoader; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.jupiter.api.Test; import org.mockito.Mockito; -@RunWith(JUnit4.class) public class GCPResourceTest { - @Rule public final WireMockRule wireMockRule = new WireMockRule(8089); - private final GCPMetadataConfig metadataConfig = new GCPMetadataConfig("http://localhost:8089/"); - private static final Map envVars = new HashMap<>(); + private static final String DUMMY_PROJECT_ID = "google-pid"; + private final ConfigProperties mockConfigProps = Mockito.mock(ConfigProperties.class); + private final Map mockGKECommonAttributes = + new HashMap<>() { + { + put(GKE_CLUSTER_NAME, "gke-cluster"); + put(GKE_HOST_ID, "host1"); + } + }; + + // Mock Platforms + private DetectedPlatform generateMockGCEPlatform() { + Map mockAttributes = + new HashMap<>() { + { + put(GCE_CLOUD_REGION, "australia-southeast1"); + put(GCE_AVAILABILITY_ZONE, "australia-southeast1-b"); + put(GCE_INSTANCE_ID, "random-id"); + put(GCE_INSTANCE_NAME, "instance-name"); + put(GCE_MACHINE_TYPE, "gce-m2"); + put(GCE_INSTANCE_HOSTNAME, "instance-hostname"); + } + }; + DetectedPlatform mockGCEPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockGCEPlatform.getSupportedPlatform()) + .thenReturn(GCPPlatformDetector.SupportedPlatform.GOOGLE_COMPUTE_ENGINE); + Mockito.when(mockGCEPlatform.getAttributes()).thenReturn(mockAttributes); + Mockito.when(mockGCEPlatform.getProjectId()).thenReturn(DUMMY_PROJECT_ID); + return mockGCEPlatform; + } + + private DetectedPlatform generateMockGKEPlatform(String gkeClusterLocationType) { + Map mockAttributes = new HashMap<>(mockGKECommonAttributes); + if (gkeClusterLocationType.equals(GKE_LOCATION_TYPE_ZONE)) { + mockAttributes.put(GKE_CLUSTER_LOCATION, "australia-southeast1-a"); + } else if (gkeClusterLocationType.equals(GKE_LOCATION_TYPE_REGION)) { + mockAttributes.put(GKE_CLUSTER_LOCATION, "australia-southeast1"); + } + mockAttributes.put(GKE_CLUSTER_LOCATION_TYPE, gkeClusterLocationType); + + DetectedPlatform mockGKEPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockGKEPlatform.getSupportedPlatform()) + .thenReturn(GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE); + Mockito.when(mockGKEPlatform.getAttributes()).thenReturn(mockAttributes); + Mockito.when(mockGKEPlatform.getProjectId()).thenReturn(DUMMY_PROJECT_ID); + return mockGKEPlatform; + } - @Before - public void clearEnvVars() { - envVars.clear(); + private DetectedPlatform generateMockServerlessPlatform( + GCPPlatformDetector.SupportedPlatform platform) { + final EnumSet serverlessPlatforms = + EnumSet.of( + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN, + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS); + if (!serverlessPlatforms.contains(platform)) { + throw new IllegalArgumentException(); + } + Map mockAttributes = + new HashMap<>() { + { + put(SERVERLESS_COMPUTE_NAME, "serverless-app"); + put(SERVERLESS_COMPUTE_REVISION, "v2"); + put(SERVERLESS_COMPUTE_INSTANCE_ID, "serverless-instance-id"); + put(SERVERLESS_COMPUTE_CLOUD_REGION, "us-central1"); + put(SERVERLESS_COMPUTE_AVAILABILITY_ZONE, "us-central1-b"); + } + }; + DetectedPlatform mockServerlessPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockServerlessPlatform.getSupportedPlatform()).thenReturn(platform); + Mockito.when(mockServerlessPlatform.getAttributes()).thenReturn(mockAttributes); + Mockito.when(mockServerlessPlatform.getProjectId()).thenReturn(DUMMY_PROJECT_ID); + return mockServerlessPlatform; + } + + private DetectedPlatform generateMockGAEPlatform() { + Map mockAttributes = + new HashMap<>() { + { + put(GAE_MODULE_NAME, "gae-app"); + put(GAE_APP_VERSION, "v1"); + put(GAE_INSTANCE_ID, "gae-instance-id"); + put(GAE_CLOUD_REGION, "us-central1"); + put(GAE_AVAILABILITY_ZONE, "us-central1-b"); + } + }; + DetectedPlatform mockGAEPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockGAEPlatform.getSupportedPlatform()) + .thenReturn(GCPPlatformDetector.SupportedPlatform.GOOGLE_APP_ENGINE); + Mockito.when(mockGAEPlatform.getAttributes()).thenReturn(mockAttributes); + Mockito.when(mockGAEPlatform.getProjectId()).thenReturn(DUMMY_PROJECT_ID); + return mockGAEPlatform; + } + + private DetectedPlatform generateMockUnknownPlatform() { + Map mockAttributes = + new HashMap<>() { + { + put(GCE_INSTANCE_ID, "instance-id"); + put(GCE_CLOUD_REGION, "australia-southeast1"); + } + }; + + DetectedPlatform mockUnknownPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockUnknownPlatform.getSupportedPlatform()) + .thenReturn(GCPPlatformDetector.SupportedPlatform.UNKNOWN_PLATFORM); + Mockito.when(mockUnknownPlatform.getAttributes()).thenReturn(mockAttributes); + return mockUnknownPlatform; } @Test - public void findsWithServiceLoader() { - ServiceLoader services = - ServiceLoader.load(ResourceProvider.class, getClass().getClassLoader()); - assertTrue( - "Could not load GCP Resource detector using serviceloader, found: " + services, - services.stream().anyMatch(provider -> provider.type().equals(GCPResource.class))); + public void testGCEResourceAttributesMapping() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockGCEPlatform(); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertEquals( + ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PLATFORM)); + assertEquals( + ResourceAttributes.CloudProviderValues.GCP, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PROVIDER)); + assertEquals( + mockPlatform.getAttributes().get(GCE_INSTANCE_ID), + gotResource.getAttributes().get(ResourceAttributes.HOST_ID)); + assertEquals( + mockPlatform.getAttributes().get(GCE_INSTANCE_NAME), + gotResource.getAttributes().get(ResourceAttributes.HOST_NAME)); + assertEquals( + mockPlatform.getAttributes().get(GCE_INSTANCE_NAME), + gotResource.getAttributes().get(ResourceAttributes.GCP_GCE_INSTANCE_NAME)); + assertEquals( + mockPlatform.getAttributes().get(GCE_INSTANCE_HOSTNAME), + gotResource.getAttributes().get(ResourceAttributes.GCP_GCE_INSTANCE_HOSTNAME)); + assertEquals( + mockPlatform.getAttributes().get(GCE_MACHINE_TYPE), + gotResource.getAttributes().get(ResourceAttributes.HOST_TYPE)); + assertEquals( + mockPlatform.getAttributes().get(GCE_AVAILABILITY_ZONE), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals( + mockPlatform.getAttributes().get(GCE_CLOUD_REGION), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertEquals(10, gotResource.getAttributes().size()); } @Test - public void testGCPComputeResourceNotGCP() { - GCPMetadataConfig mockMetadataConfig = Mockito.mock(GCPMetadataConfig.class); - Mockito.when(mockMetadataConfig.isRunningOnGcp()).thenReturn(false); + public void testGKEResourceAttributesMapping_LocationTypeRegion() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockGKEPlatform(GKE_LOCATION_TYPE_REGION); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + + verifyGKEMapping(gotResource, mockPlatform); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals( + mockPlatform.getAttributes().get(GKE_CLUSTER_LOCATION), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertEquals(6, gotResource.getAttributes().size()); + } + + @Test + public void testGKEResourceAttributesMapping_LocationTypeZone() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockGKEPlatform(GKE_LOCATION_TYPE_ZONE); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); - GCPResource testResource = new GCPResource(mockMetadataConfig, EnvVars.DEFAULT_INSTANCE); - // If GCPMetadataConfig determines that its not running on GCP, then attributes should be empty - assertThat(testResource.getAttributes()).isEmpty(); + verifyGKEMapping(gotResource, mockPlatform); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertEquals( + mockPlatform.getAttributes().get(GKE_CLUSTER_LOCATION), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals(6, gotResource.getAttributes().size()); } @Test - public void testGCPComputeResourceNonGCPEndpoint() { - // intentionally not providing the required Metadata-Flovor header with the - // request to mimic non GCP endpoint - stubFor( - get(urlEqualTo("/project/project-id")) - .willReturn(aResponse().withBody("nonGCPendpointTest"))); - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()).isEmpty(); + public void testGKEResourceAttributesMapping_LocationTypeInvalid() { + Map mockGKEAttributes = new HashMap<>(mockGKECommonAttributes); + mockGKEAttributes.put(GKE_CLUSTER_LOCATION_TYPE, "INVALID"); + mockGKEAttributes.put(GKE_CLUSTER_LOCATION, "some-location"); + + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = Mockito.mock(DetectedPlatform.class); + Mockito.when(mockPlatform.getSupportedPlatform()) + .thenReturn(GCPPlatformDetector.SupportedPlatform.GOOGLE_KUBERNETES_ENGINE); + Mockito.when(mockPlatform.getProjectId()).thenReturn(DUMMY_PROJECT_ID); + Mockito.when(mockPlatform.getAttributes()).thenReturn(mockGKEAttributes); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + + verifyGKEMapping(gotResource, mockPlatform); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals(5, gotResource.getAttributes().size()); } - /** Google Compute Engine Tests * */ @Test - public void testGCEResourceWithGCEAttributesSucceeds() { - stubEndpoint("/project/project-id", "GCE-pid"); - stubEndpoint("/instance/zone", "country-region-zone"); - stubEndpoint("/instance/id", "GCE-instance-id"); - stubEndpoint("/instance/name", "GCE-instance-name"); - stubEndpoint("/instance/machine-type", "GCE-instance-type"); - - final GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()) - .hasSize(8) - .containsEntry( - ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP) - .containsEntry( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_COMPUTE_ENGINE) - .containsEntry(ResourceAttributes.CLOUD_ACCOUNT_ID, "GCE-pid") - .containsEntry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone") - .containsEntry(ResourceAttributes.CLOUD_REGION, "country-region") - .containsEntry(ResourceAttributes.HOST_ID, "GCE-instance-id") - .containsEntry(ResourceAttributes.HOST_NAME, "GCE-instance-name") - .containsEntry(ResourceAttributes.HOST_TYPE, "GCE-instance-type"); - } - - /** Google Kubernetes Engine Tests * */ + public void testGKEResourceAttributesMapping_LocationMissing() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockGKEPlatform(""); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + + verifyGKEMapping(gotResource, mockPlatform); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertNull(gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals(5, gotResource.getAttributes().size()); + } + + private void verifyGKEMapping(Resource gotResource, DetectedPlatform detectedPlatform) { + assertEquals( + ResourceAttributes.CloudPlatformValues.GCP_KUBERNETES_ENGINE, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PLATFORM)); + assertEquals( + ResourceAttributes.CloudProviderValues.GCP, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PROVIDER)); + assertEquals( + detectedPlatform.getAttributes().get(GKE_HOST_ID), + gotResource.getAttributes().get(ResourceAttributes.HOST_ID)); + assertEquals( + detectedPlatform.getAttributes().get(GKE_CLUSTER_NAME), + gotResource.getAttributes().get(ResourceAttributes.K8S_CLUSTER_NAME)); + } + @Test - public void testGKEResourceWithGKEAttributesSucceedsLocationZone() { - envVars.put("KUBERNETES_SERVICE_HOST", "GKE-testHost"); - envVars.put("NAMESPACE", "GKE-testNameSpace"); - // Hostname can truncate pod name, so we test downward API override. - envVars.put("HOSTNAME", "GKE-testHostName"); - envVars.put("POD_NAME", "GKE-testHostName-full-1234"); - envVars.put("CONTAINER_NAME", "GKE-testContainerName"); - - stubEndpoint("/project/project-id", "GCE-pid"); - stubEndpoint("/instance/id", "GCE-instance-id"); - stubEndpoint("/instance/name", "GCE-instance-name"); - stubEndpoint("/instance/machine-type", "GCE-instance-type"); - stubEndpoint("/instance/attributes/cluster-name", "GKE-cluster-name"); - stubEndpoint("/instance/attributes/cluster-location", "country-region-zone"); - - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()) - .hasSize(8) - .containsEntry(ResourceAttributes.CLOUD_PROVIDER, "gcp") - .containsEntry(ResourceAttributes.CLOUD_PLATFORM, "gcp_kubernetes_engine") - .containsEntry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone") - .containsEntry(ResourceAttributes.HOST_ID, "GCE-instance-id") - .containsEntry(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name") - .containsEntry(ResourceAttributes.K8S_NAMESPACE_NAME, "GKE-testNameSpace") - .containsEntry(ResourceAttributes.K8S_POD_NAME, "GKE-testHostName-full-1234") - .containsEntry(ResourceAttributes.K8S_CONTAINER_NAME, "GKE-testContainerName"); + public void testGCRResourceAttributesMapping() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = + generateMockServerlessPlatform(GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_RUN); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + assertEquals( + ResourceAttributes.CloudPlatformValues.GCP_CLOUD_RUN, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PLATFORM)); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + verifyServerlessMapping(gotResource, mockPlatform); + assertEquals(8, gotResource.getAttributes().size()); } @Test - public void testGKEResourceWithGKEAttributesSucceedsLocationRegion() { - envVars.put("KUBERNETES_SERVICE_HOST", "GKE-testHost"); - envVars.put("NAMESPACE", "GKE-testNameSpace"); - // Hostname can truncate pod name, so we test downward API override. - envVars.put("HOSTNAME", "GKE-testHostName"); - envVars.put("POD_NAME", "GKE-testHostName-full-1234"); - envVars.put("CONTAINER_NAME", "GKE-testContainerName"); - - stubEndpoint("/project/project-id", "GCE-pid"); - stubEndpoint("/instance/id", "GCE-instance-id"); - stubEndpoint("/instance/name", "GCE-instance-name"); - stubEndpoint("/instance/machine-type", "GCE-instance-type"); - stubEndpoint("/instance/attributes/cluster-name", "GKE-cluster-name"); - stubEndpoint("/instance/attributes/cluster-location", "country-region"); - - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()) - .hasSize(8) - .containsEntry(ResourceAttributes.CLOUD_PROVIDER, "gcp") - .containsEntry(ResourceAttributes.CLOUD_PLATFORM, "gcp_kubernetes_engine") - .containsEntry(ResourceAttributes.CLOUD_REGION, "country-region") - .containsEntry(ResourceAttributes.HOST_ID, "GCE-instance-id") - .containsEntry(ResourceAttributes.K8S_CLUSTER_NAME, "GKE-cluster-name") - .containsEntry(ResourceAttributes.K8S_NAMESPACE_NAME, "GKE-testNameSpace") - .containsEntry(ResourceAttributes.K8S_POD_NAME, "GKE-testHostName-full-1234") - .containsEntry(ResourceAttributes.K8S_CONTAINER_NAME, "GKE-testContainerName"); - } - - /** Google Cloud Functions Tests * */ + public void testGCFResourceAttributeMapping() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = + generateMockServerlessPlatform( + GCPPlatformDetector.SupportedPlatform.GOOGLE_CLOUD_FUNCTIONS); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + assertEquals( + ResourceAttributes.CloudPlatformValues.GCP_CLOUD_FUNCTIONS, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PLATFORM)); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + verifyServerlessMapping(gotResource, mockPlatform); + assertEquals(8, gotResource.getAttributes().size()); + } + + private void verifyServerlessMapping(Resource gotResource, DetectedPlatform detectedPlatform) { + assertEquals( + ResourceAttributes.CloudProviderValues.GCP, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PROVIDER)); + assertEquals( + detectedPlatform.getAttributes().get(SERVERLESS_COMPUTE_NAME), + gotResource.getAttributes().get(ResourceAttributes.FAAS_NAME)); + assertEquals( + detectedPlatform.getAttributes().get(SERVERLESS_COMPUTE_REVISION), + gotResource.getAttributes().get(ResourceAttributes.FAAS_VERSION)); + assertEquals( + detectedPlatform.getAttributes().get(SERVERLESS_COMPUTE_INSTANCE_ID), + gotResource.getAttributes().get(ResourceAttributes.FAAS_INSTANCE)); + assertEquals( + detectedPlatform.getAttributes().get(SERVERLESS_COMPUTE_AVAILABILITY_ZONE), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals( + detectedPlatform.getAttributes().get(SERVERLESS_COMPUTE_CLOUD_REGION), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + } + @Test - public void testGCFResourceWithCloudFunctionAttributesSucceeds() { - // Setup GCF required env vars - envVars.put("K_SERVICE", "cloud-function-hello"); - envVars.put("K_REVISION", "cloud-function-hello.1"); - envVars.put("FUNCTION_TARGET", "cloud-function-hello"); - - stubEndpoint("/project/project-id", "GCF-pid"); - stubEndpoint("/instance/zone", "country-region-zone"); - stubEndpoint("/instance/id", "GCF-instance-id"); - - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()) - .hasSize(7) - .containsEntry( - ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP) - .containsEntry( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_CLOUD_FUNCTIONS) - .containsEntry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone") - .containsEntry(ResourceAttributes.CLOUD_REGION, "country-region") - .containsEntry(ResourceAttributes.FAAS_NAME, envVars.get("K_SERVICE")) - .containsEntry(ResourceAttributes.FAAS_VERSION, envVars.get("K_REVISION")) - .containsEntry(ResourceAttributes.FAAS_INSTANCE, "GCF-instance-id"); - } - - /** Google App Engine Tests * */ + public void testGAEResourceAttributeMapping() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockGAEPlatform(); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + assertEquals( + ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PLATFORM)); + assertEquals( + ResourceAttributes.CloudProviderValues.GCP, + gotResource.getAttributes().get(ResourceAttributes.CLOUD_PROVIDER)); + assertEquals( + mockPlatform.getProjectId(), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_ACCOUNT_ID)); + assertEquals( + mockPlatform.getAttributes().get(GAE_MODULE_NAME), + gotResource.getAttributes().get(ResourceAttributes.FAAS_NAME)); + assertEquals( + mockPlatform.getAttributes().get(GAE_APP_VERSION), + gotResource.getAttributes().get(ResourceAttributes.FAAS_VERSION)); + assertEquals( + mockPlatform.getAttributes().get(GAE_INSTANCE_ID), + gotResource.getAttributes().get(ResourceAttributes.FAAS_INSTANCE)); + assertEquals( + mockPlatform.getAttributes().get(GAE_AVAILABILITY_ZONE), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_AVAILABILITY_ZONE)); + assertEquals( + mockPlatform.getAttributes().get(GAE_CLOUD_REGION), + gotResource.getAttributes().get(ResourceAttributes.CLOUD_REGION)); + assertEquals(8, gotResource.getAttributes().size()); + } + @Test - public void testGAEResourceWithAppEngineAttributesSucceedsInFlex() { - envVars.put("GAE_SERVICE", "app-engine-hello"); - envVars.put("GAE_VERSION", "app-engine-hello-v1"); - envVars.put("GAE_INSTANCE", "app-engine-hello-f236d"); - - stubEndpoint("/project/project-id", "GAE-pid-flex"); - // for flex, the region should be parsed from zone attribute - stubEndpoint("/instance/zone", "country-region-zone"); - stubEndpoint("/instance/region", "country-region1"); - stubEndpoint("/instance/id", "GAE-instance-id"); - - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(envVars)); - assertThat(testResource.getAttributes()) - .hasSize(7) - .containsEntry( - ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP) - .containsEntry( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE) - .containsEntry(ResourceAttributes.CLOUD_REGION, "country-region") - .containsEntry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone") - .containsEntry(ResourceAttributes.FAAS_NAME, envVars.get("GAE_SERVICE")) - .containsEntry(ResourceAttributes.FAAS_VERSION, envVars.get("GAE_VERSION")) - .containsEntry(ResourceAttributes.FAAS_INSTANCE, envVars.get("GAE_INSTANCE")); + public void testUnknownPlatformResourceAttributesMapping() { + GCPPlatformDetector mockDetector = Mockito.mock(GCPPlatformDetector.class); + DetectedPlatform mockPlatform = generateMockUnknownPlatform(); + Mockito.when(mockDetector.detectPlatform()).thenReturn(mockPlatform); + + Resource gotResource = new GCPResource(mockDetector).createResource(mockConfigProps); + assertTrue(gotResource.getAttributes().isEmpty(), "no attributes for unknown platform"); } @Test - public void testGAEResourceWithAppEngineAttributesSucceedsInStandard() { - envVars.put("GAE_SERVICE", "app-engine-hello"); - envVars.put("GAE_VERSION", "app-engine-hello-v1"); - envVars.put("GAE_INSTANCE", "app-engine-hello-f236d"); - - stubEndpoint("/project/project-id", "GAE-pid-standard"); - // for standard, the region should be extracted from region attribute - stubEndpoint("/instance/zone", "country-region-zone"); - stubEndpoint("/instance/region", "country-region1"); - stubEndpoint("/instance/id", "GAE-instance-id"); - - Map updatedEnvVars = new HashMap<>(envVars); - updatedEnvVars.put("GAE_ENV", "standard"); - GCPResource testResource = new GCPResource(metadataConfig, new EnvVarMock(updatedEnvVars)); - assertThat(testResource.getAttributes()) - .hasSize(7) - .containsEntry( - ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.GCP) - .containsEntry( - ResourceAttributes.CLOUD_PLATFORM, - ResourceAttributes.CloudPlatformValues.GCP_APP_ENGINE) - .containsEntry(ResourceAttributes.CLOUD_REGION, "country-region1") - .containsEntry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "country-region-zone") - .containsEntry(ResourceAttributes.FAAS_NAME, envVars.get("GAE_SERVICE")) - .containsEntry(ResourceAttributes.FAAS_VERSION, envVars.get("GAE_VERSION")) - .containsEntry(ResourceAttributes.FAAS_INSTANCE, envVars.get("GAE_INSTANCE")); - } - - // Helper method to help stub endpoints - private void stubEndpoint(String endpointPath, String responseBody) { - stubFor( - get(urlEqualTo(endpointPath)) - .willReturn( - aResponse().withHeader("Metadata-Flavor", "Google").withBody(responseBody))); + public void findsWithServiceLoader() { + ServiceLoader services = + ServiceLoader.load(ResourceProvider.class, getClass().getClassLoader()); + assertTrue( + services.stream().anyMatch(provider -> provider.type().equals(GCPResource.class)), + "Could not load GCP Resource detector using serviceloader, found: " + services); } } diff --git a/examples/resource/src/main/java/com/google/cloud/opentelemetry/example/resource/ResourceExample.java b/examples/resource/src/main/java/com/google/cloud/opentelemetry/example/resource/ResourceExample.java index b465fd19..37a59cf1 100644 --- a/examples/resource/src/main/java/com/google/cloud/opentelemetry/example/resource/ResourceExample.java +++ b/examples/resource/src/main/java/com/google/cloud/opentelemetry/example/resource/ResourceExample.java @@ -25,7 +25,7 @@ public static void main(String[] args) { Resource autoResource = ResourceConfiguration.createEnvironmentResource(); System.out.println(autoResource.getAttributes()); System.out.println("Detecting resource: hardcoded"); - GCPResource resource = new GCPResource(); - System.out.println(resource.getAttributes()); + GCPResource resourceProvider = new GCPResource(); + System.out.println(resourceProvider.getAttributes()); } } diff --git a/settings.gradle b/settings.gradle index 52b92e2d..0947a568 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,6 +33,7 @@ include ":examples-autoconf" include ":examples-autoinstrument" include ":examples-resource" include ":detector-resources" +include ":detector-resources-support" include ":e2e-test-server" include ":examples-spring" include ":propagators-gcp" @@ -56,6 +57,9 @@ project(':exporter-auto').projectDir = project(':detector-resources').projectDir = "$rootDir/detectors/resources" as File +project(':detector-resources-support').projectDir = + "$rootDir/detectors/resources-support" as File + project(':examples-resource').projectDir = "$rootDir/examples/resource" as File