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