diff --git a/azure-resources/README.md b/azure-resources/README.md new file mode 100644 index 000000000..2428b1394 --- /dev/null +++ b/azure-resources/README.md @@ -0,0 +1,34 @@ +# Azure Resource Detectors for OpenTelemetry + +This module provides Azure resource detectors for OpenTelemetry. + +The following OpenTelemetry semantic conventions will be detected: + +| Resource attribute | VM | Functions | App Service | Containers | +|-------------------------|----------|-----------------|-------------------|----------------------| +| cloud.platform | azure_vm | azure_functions | azure_app_service | azure_container_apps | +| cloud.provider | azure | azure | azure | azure | +| cloud.resource.id | auto | | auto | | +| cloud.region | auto | auto | auto | | +| deployment.environment | | | auto | | +| host.id | auto | | auto | | +| host.name | auto | | | | +| host.type | auto | | | | +| os.type | auto | | | | +| os.version | auto | | | | +| azure.vm.scaleset.name | auto | | | | +| azure.vm.sku | auto | | | | +| service.name | | | auto | auto | +| service.version | | | | auto | +| service.instance.id | | | auto | auto | +| azure.app.service.stamp | | | auto | | +| faas.name | | auto | | | +| faas.version | | auto | | | +| faas.instance | | auto | | | +| faas.faas.max_memory | | auto | | | + +## Component Owners + +TODO + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/azure-resources/build.gradle.kts b/azure-resources/build.gradle.kts new file mode 100644 index 000000000..9cfff9133 --- /dev/null +++ b/azure-resources/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + id("otel.java-conventions") + + id("otel.publish-conventions") + id("maven-publish") +} + +description = "OpenTelemetry GCP Resources Support" +otelJava.moduleName.set("io.opentelemetry.contrib.gcp.resource") + +// enable publishing to maven local +java { + withSourcesJar() +} + +dependencies { + api("io.opentelemetry:opentelemetry-api") + api("io.opentelemetry:opentelemetry-sdk") + + implementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") + + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + + implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.squareup.okhttp3:okhttp") + + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-testing") + +// testImplementation("org.mockito:mockito-core") + testImplementation("com.google.guava:guava") + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.assertj:assertj-core") + testImplementation("com.linecorp.armeria:armeria-junit5") +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProvider.java new file mode 100644 index 000000000..2b09174a8 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.K8sIncubatingAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +public class AzureAksResourceProvider extends CloudResourceProvider { + + private static final Map COMPUTE_MAPPING = new HashMap<>(); + + static { + COMPUTE_MAPPING.put( + "resourceGroupName", + new AzureVmResourceProvider.Entry( + K8sIncubatingAttributes.K8S_CLUSTER_NAME, AzureAksResourceProvider::parseClusterName)); + } + + // visible for testing + static String parseClusterName(String resourceGroup) { + // Code inspired by + // https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/datadogexporter/internal/hostmetadata/internal/azure/provider.go#L36 + String[] splitAll = resourceGroup.split("_"); + if (splitAll.length == 4 && splitAll[0].equalsIgnoreCase("mc")) { + return splitAll[splitAll.length - 2]; + } + return resourceGroup; + } + + // Environment variable that is set when running on Kubernetes + static final String KUBERNETES_SERVICE_HOST = "KUBERNETES_SERVICE_HOST"; + private final Supplier> client; + private final Map environment; + + // SPI + public AzureAksResourceProvider() { + this(AzureMetadataService.defaultClient(), System.getenv()); + } + + // visible for testing + public AzureAksResourceProvider( + Supplier> client, Map environment) { + this.client = client; + this.environment = environment; + } + + @Override + public int order() { + // run after the fast cloud resource providers that only check environment variables + // and before the AKS provider + return 100; + } + + @Override + public Resource createResource(ConfigProperties configProperties) { + if (environment.get(KUBERNETES_SERVICE_HOST) == null) { + return Resource.empty(); + } + return client + .get() + .map( + body -> + AzureVmResourceProvider.parseMetadata( + body, COMPUTE_MAPPING, CloudIncubatingAttributes.CloudPlatformValues.AZURE_AKS)) + .orElse(Resource.empty()); + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProvider.java new file mode 100644 index 000000000..cc77f3df0 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProvider.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_REGION; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; +import static io.opentelemetry.semconv.incubating.DeploymentIncubatingAttributes.DEPLOYMENT_ENVIRONMENT; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_ID; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_INSTANCE_ID; + +import io.opentelemetry.api.common.AttributeKey; +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.resources.Resource; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nullable; + +public class AzureAppServiceResourceProvider extends CloudResourceProvider { + + static final AttributeKey AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE = + AttributeKey.stringKey("azure.app.service.stamp"); + static final String REGION_NAME = "REGION_NAME"; + private static final String WEBSITE_HOME_STAMPNAME = "WEBSITE_HOME_STAMPNAME"; + private static final String WEBSITE_HOSTNAME = "WEBSITE_HOSTNAME"; + static final String WEBSITE_INSTANCE_ID = "WEBSITE_INSTANCE_ID"; + private static final String WEBSITE_OWNER_NAME = "WEBSITE_OWNER_NAME"; + private static final String WEBSITE_RESOURCE_GROUP = "WEBSITE_RESOURCE_GROUP"; + static final String WEBSITE_SITE_NAME = "WEBSITE_SITE_NAME"; + private static final String WEBSITE_SLOT_NAME = "WEBSITE_SLOT_NAME"; + + private static final Map, String> ENV_VAR_MAPPING = new HashMap<>(); + + static { + ENV_VAR_MAPPING.put(CLOUD_REGION, REGION_NAME); + ENV_VAR_MAPPING.put(DEPLOYMENT_ENVIRONMENT, WEBSITE_SLOT_NAME); + ENV_VAR_MAPPING.put(HOST_ID, WEBSITE_HOSTNAME); + ENV_VAR_MAPPING.put(SERVICE_INSTANCE_ID, WEBSITE_INSTANCE_ID); + ENV_VAR_MAPPING.put(AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE, WEBSITE_HOME_STAMPNAME); + } + + private final Map env; + + // SPI + public AzureAppServiceResourceProvider() { + this(System.getenv()); + } + + // Visible for testing + AzureAppServiceResourceProvider(Map env) { + this.env = env; + } + + @Override + public Resource createResource(ConfigProperties config) { + return Resource.create(getAttributes()); + } + + public Attributes getAttributes() { + AzureEnvVarPlatform detect = AzureEnvVarPlatform.detect(env); + if (detect != AzureEnvVarPlatform.APP_SERVICE) { + return Attributes.empty(); + } + String name = Objects.requireNonNull(env.get(WEBSITE_SITE_NAME)); + AttributesBuilder builder = + AzureVmResourceProvider.azureAttributeBuilder( + CloudIncubatingAttributes.CloudPlatformValues.AZURE_APP_SERVICE); + builder.put(SERVICE_NAME, name); + + String resourceUri = resourceUri(name); + if (resourceUri != null) { + builder.put(CLOUD_RESOURCE_ID, resourceUri); + } + + AzureEnvVarPlatform.addAttributesFromEnv(ENV_VAR_MAPPING, env, builder); + + return builder.build(); + } + + @Nullable + private String resourceUri(String websiteName) { + String websiteResourceGroup = env.get(WEBSITE_RESOURCE_GROUP); + String websiteOwnerName = env.get(WEBSITE_OWNER_NAME); + + String subscriptionId; + if (websiteOwnerName != null && websiteOwnerName.contains("+")) { + subscriptionId = websiteOwnerName.substring(0, websiteOwnerName.indexOf("+")); + } else { + subscriptionId = websiteOwnerName; + } + + if (StringUtils.isNullOrEmpty(websiteResourceGroup) + || StringUtils.isNullOrEmpty(subscriptionId)) { + return null; + } + + return String.format( + "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Web/sites/%s", + subscriptionId, websiteResourceGroup, websiteName); + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProvider.java new file mode 100644 index 000000000..be588e0ff --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProvider.java @@ -0,0 +1,65 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_VERSION; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_INSTANCE_ID; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import java.util.HashMap; +import java.util.Map; + +public class AzureContainersResourceProvider extends CloudResourceProvider { + + static final String CONTAINER_APP_NAME = "CONTAINER_APP_NAME"; + + private static final String CONTAINER_APP_REPLICA_NAME = "CONTAINER_APP_REPLICA_NAME"; + private static final String CONTAINER_APP_REVISION = "CONTAINER_APP_REVISION"; + + private static final Map, String> ENV_VAR_MAPPING = new HashMap<>(); + + static { + ENV_VAR_MAPPING.put(SERVICE_NAME, CONTAINER_APP_NAME); + ENV_VAR_MAPPING.put(SERVICE_INSTANCE_ID, CONTAINER_APP_REPLICA_NAME); + ENV_VAR_MAPPING.put(SERVICE_VERSION, CONTAINER_APP_REVISION); + } + + private final Map env; + + // SPI + public AzureContainersResourceProvider() { + this(System.getenv()); + } + + // Visible for testing + AzureContainersResourceProvider(Map env) { + this.env = env; + } + + @Override + public Resource createResource(ConfigProperties config) { + return Resource.create(getAttributes()); + } + + public Attributes getAttributes() { + AzureEnvVarPlatform detect = AzureEnvVarPlatform.detect(env); + if (detect != AzureEnvVarPlatform.CONTAINER_APP) { + return Attributes.empty(); + } + + AttributesBuilder builder = + AzureVmResourceProvider.azureAttributeBuilder("azure_container_apps"); + + AzureEnvVarPlatform.addAttributesFromEnv(ENV_VAR_MAPPING, env, builder); + + return builder.build(); + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureEnvVarPlatform.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureEnvVarPlatform.java new file mode 100644 index 000000000..257f05ba8 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureEnvVarPlatform.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.AttributesBuilder; +import java.util.Map; + +public enum AzureEnvVarPlatform { + APP_SERVICE, + FUNCTIONS, + CONTAINER_APP, + NONE; + + public static AzureEnvVarPlatform detect(Map env) { + String appName = env.get(AzureContainersResourceProvider.CONTAINER_APP_NAME); + if (appName != null) { + return CONTAINER_APP; + } + String name = env.get(AzureAppServiceResourceProvider.WEBSITE_SITE_NAME); + if (name == null) { + return NONE; + } + if (env.get(AzureFunctionsResourceProvider.FUNCTIONS_VERSION) != null) { + return FUNCTIONS; + } + return APP_SERVICE; + } + + static void addAttributesFromEnv( + Map, String> mapping, + Map env, + AttributesBuilder builder) { + mapping.forEach( + (key, value) -> { + String envValue = env.get(value); + if (envValue != null) { + builder.put(key, envValue); + } + }); + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProvider.java new file mode 100644 index 000000000..0d6504325 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INSTANCE; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_MAX_MEMORY; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_NAME; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_VERSION; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import java.util.HashMap; +import java.util.Map; + +public class AzureFunctionsResourceProvider extends CloudResourceProvider { + + static final String FUNCTIONS_VERSION = "FUNCTIONS_EXTENSION_VERSION"; + private static final String FUNCTIONS_MEM_LIMIT = "WEBSITE_MEMORY_LIMIT_MB"; + + private static final Map, String> ENV_VAR_MAPPING = new HashMap<>(); + + static { + ENV_VAR_MAPPING.put( + CloudIncubatingAttributes.CLOUD_REGION, AzureAppServiceResourceProvider.REGION_NAME); + ENV_VAR_MAPPING.put(FAAS_NAME, AzureAppServiceResourceProvider.WEBSITE_SITE_NAME); + ENV_VAR_MAPPING.put(FAAS_VERSION, FUNCTIONS_VERSION); + ENV_VAR_MAPPING.put(FAAS_INSTANCE, AzureAppServiceResourceProvider.WEBSITE_INSTANCE_ID); + } + + private final Map env; + + // SPI + public AzureFunctionsResourceProvider() { + this(System.getenv()); + } + + // Visible for testing + AzureFunctionsResourceProvider(Map env) { + this.env = env; + } + + @Override + public Resource createResource(ConfigProperties config) { + return Resource.create(getAttributes()); + } + + public Attributes getAttributes() { + AzureEnvVarPlatform detect = AzureEnvVarPlatform.detect(env); + if (detect != AzureEnvVarPlatform.FUNCTIONS) { + return Attributes.empty(); + } + + AttributesBuilder builder = + AzureVmResourceProvider.azureAttributeBuilder( + CloudIncubatingAttributes.CloudPlatformValues.AZURE_FUNCTIONS); + + String limit = env.get(FUNCTIONS_MEM_LIMIT); + if (limit != null) { + builder.put(FAAS_MAX_MEMORY, Long.parseLong(limit)); + } + + AzureEnvVarPlatform.addAttributesFromEnv(ENV_VAR_MAPPING, env, builder); + + return builder.build(); + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureMetadataService.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureMetadataService.java new file mode 100644 index 000000000..07f67e240 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureMetadataService.java @@ -0,0 +1,75 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + +public class AzureMetadataService { + static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final URL METADATA_URL; + + static { + try { + METADATA_URL = new URL("http://169.254.169.254/metadata/instance?api-version=2021-02-01"); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + private AzureMetadataService() {} + + private static final Duration TIMEOUT = Duration.ofSeconds(1); + + private static final Logger logger = Logger.getLogger(AzureMetadataService.class.getName()); + + static Supplier> defaultClient() { + return () -> fetchMetadata(METADATA_URL); + } + + // visible for testing + static Optional fetchMetadata(URL url) { + OkHttpClient client = + new OkHttpClient.Builder() + .callTimeout(TIMEOUT) + .connectTimeout(TIMEOUT) + .readTimeout(TIMEOUT) + .build(); + + Request request = new Request.Builder().url(url).get().addHeader("Metadata", "true").build(); + + try (Response response = client.newCall(request).execute()) { + int responseCode = response.code(); + if (responseCode != 200) { + logger.log( + Level.FINE, + "Error response from " + + url + + " code (" + + responseCode + + ") text " + + response.message()); + return Optional.empty(); + } + + return Optional.of(Objects.requireNonNull(response.body()).string()); + } catch (IOException e) { + logger.log(Level.FINE, "Failed to fetch Azure VM metadata", e); + return Optional.empty(); + } + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProvider.java new file mode 100644 index 000000000..059ff22f8 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProvider.java @@ -0,0 +1,167 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_REGION; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_ID; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_NAME; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_TYPE; +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_TYPE; +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_VERSION; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.jetbrains.annotations.NotNull; + +public class AzureVmResourceProvider extends CloudResourceProvider { + + static class Entry { + final AttributeKey key; + final Function transform; + + Entry(AttributeKey key) { + this(key, Function.identity()); + } + + Entry(AttributeKey key, Function transform) { + this.key = key; + this.transform = transform; + } + } + + private static final Map COMPUTE_MAPPING = new HashMap<>(); + + static { + COMPUTE_MAPPING.put("location", new Entry(CLOUD_REGION)); + COMPUTE_MAPPING.put("resourceId", new Entry(CLOUD_RESOURCE_ID)); + COMPUTE_MAPPING.put("vmId", new Entry(HOST_ID)); + COMPUTE_MAPPING.put("name", new Entry(HOST_NAME)); + COMPUTE_MAPPING.put("vmSize", new Entry(HOST_TYPE)); + COMPUTE_MAPPING.put("osType", new Entry(OS_TYPE)); + COMPUTE_MAPPING.put("version", new Entry(OS_VERSION)); + COMPUTE_MAPPING.put( + "vmScaleSetName", new Entry(AttributeKey.stringKey("azure.vm.scaleset.name"))); + COMPUTE_MAPPING.put("sku", new Entry(AttributeKey.stringKey("azure.vm.sku"))); + } + + private static final Logger logger = Logger.getLogger(AzureVmResourceProvider.class.getName()); + + private final Supplier> client; + + // SPI + public AzureVmResourceProvider() { + this(AzureMetadataService.defaultClient()); + } + + // visible for testing + public AzureVmResourceProvider(Supplier> client) { + this.client = client; + } + + @Override + public int order() { + // run after the fast cloud resource providers that only check environment variables + // and after the AKS provider + return 100; + } + + @Override + public Resource createResource(ConfigProperties config) { + return client + .get() + .map( + body -> + parseMetadata( + body, COMPUTE_MAPPING, CloudIncubatingAttributes.CloudPlatformValues.AZURE_VM)) + .orElse(Resource.empty()); + } + + static Resource parseMetadata(String body, Map computeMapping, String platform) { + AttributesBuilder builder = azureAttributeBuilder(platform); + try (JsonParser parser = AzureMetadataService.JSON_FACTORY.createParser(body)) { + parser.nextToken(); + parseResponse(parser, builder, computeMapping); + } catch (IOException e) { + logger.log(Level.FINE, "Can't get Azure VM metadata", e); + } + return Resource.create(builder.build()); + } + + @NotNull + static AttributesBuilder azureAttributeBuilder(String platform) { + AttributesBuilder builder = Attributes.builder(); + builder.put(CLOUD_PROVIDER, CloudIncubatingAttributes.CloudProviderValues.AZURE); + builder.put(CLOUD_PLATFORM, platform); + return builder; + } + + static void parseResponse( + JsonParser parser, AttributesBuilder builder, Map computeMapping) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + logger.log(Level.FINE, "Couldn't parse ECS metadata, invalid JSON"); + return; + } + + consumeJson( + parser, + (name, value) -> { + try { + if (name.equals("compute")) { + consumeCompute(parser, builder, computeMapping); + } else { + parser.skipChildren(); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + + private static void consumeCompute( + JsonParser parser, AttributesBuilder builder, Map computeMapping) + throws IOException { + consumeJson( + parser, + (computeName, computeValue) -> { + Entry entry = computeMapping.get(computeName); + if (entry != null) { + builder.put(entry.key, entry.transform.apply(computeValue)); + } else { + try { + parser.skipChildren(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + }); + } + + private static void consumeJson(JsonParser parser, BiConsumer consumer) + throws IOException { + while (parser.nextToken() != JsonToken.END_OBJECT) { + consumer.accept(parser.currentName(), parser.nextTextValue()); + } + } +} diff --git a/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/CloudResourceProvider.java b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/CloudResourceProvider.java new file mode 100644 index 000000000..96fdeef27 --- /dev/null +++ b/azure-resources/src/main/java/io/opentelemetry/contrib/azure/resource/CloudResourceProvider.java @@ -0,0 +1,20 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConditionalResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +public abstract class CloudResourceProvider implements ConditionalResourceProvider { + + @Override + public final boolean shouldApply(ConfigProperties config, Resource existing) { + return existing.getAttribute(CLOUD_PROVIDER) == null; + } +} diff --git a/azure-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/azure-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 000000000..19104d8c4 --- /dev/null +++ b/azure-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1,3 @@ +io.opentelemetry.contrib.azure.resource.AzureAppServiceResourceProvider +io.opentelemetry.contrib.azure.resource.AzureFunctionsResourceProvider +io.opentelemetry.contrib.azure.resource.AzureVmResourceProvider diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProviderTest.java new file mode 100644 index 000000000..81787ebc1 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAksResourceProviderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import io.opentelemetry.semconv.incubating.K8sIncubatingAttributes; +import java.util.Collections; +import java.util.Optional; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +class AzureAksResourceProviderTest extends MetadataBasedResourceProviderTest { + + @NotNull + @Override + protected ResourceProvider getResourceProvider(Supplier> client) { + return new AzureAksResourceProvider( + client, + Collections.singletonMap(AzureAksResourceProvider.KUBERNETES_SERVICE_HOST, "localhost")); + } + + @Override + protected String getPlatform() { + return CloudIncubatingAttributes.CloudPlatformValues.AZURE_AKS; + } + + @Override + protected void assertDefaultAttributes(AttributesAssert attributesAssert) { + attributesAssert + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, CloudIncubatingAttributes.CloudPlatformValues.AZURE_AKS) + .containsEntry(K8sIncubatingAttributes.K8S_CLUSTER_NAME, "macikgo-test-may-23"); + } + + @Test + void notOnK8s() { + AzureAksResourceProvider provider = + new AzureAksResourceProvider(() -> Optional.of(okResponse()), Collections.emptyMap()); + Attributes attributes = provider.createResource(null).getAttributes(); + OpenTelemetryAssertions.assertThat(attributes).isEmpty(); + } + + @Test + void parseClusterName() { + String clusterName = + AzureAksResourceProvider.parseClusterName( + "mc_macikgo-test-may-23_macikgo-test-may-23_eastus"); + assertThat(clusterName).isEqualTo("macikgo-test-may-23"); + } +} diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProviderTest.java new file mode 100644 index 000000000..936c6b7c7 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureAppServiceResourceProviderTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_REGION; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; +import static io.opentelemetry.semconv.incubating.DeploymentIncubatingAttributes.DEPLOYMENT_ENVIRONMENT; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_ID; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_INSTANCE_ID; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +class AzureAppServiceResourceProviderTest { + + private static final String TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME"; + private static final String TEST_REGION_NAME = "TEST_REGION_NAME"; + private static final String TEST_WEBSITE_SLOT_NAME = "TEST_WEBSITE_SLOT_NAME"; + private static final String TEST_WEBSITE_HOSTNAME = "TEST_WEBSITE_HOSTNAME"; + private static final String TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID"; + private static final String TEST_WEBSITE_HOME_STAMPNAME = "TEST_WEBSITE_HOME_STAMPNAME"; + private static final String TEST_WEBSITE_RESOURCE_GROUP = "TEST_WEBSITE_RESOURCE_GROUP"; + private static final String TEST_WEBSITE_OWNER_NAME = "TEST_WEBSITE_OWNER_NAME"; + private static final ImmutableMap DEFAULT_ENV_VARS = + ImmutableMap.of( + "WEBSITE_SITE_NAME", TEST_WEBSITE_SITE_NAME, + "REGION_NAME", TEST_REGION_NAME, + "WEBSITE_SLOT_NAME", TEST_WEBSITE_SLOT_NAME, + "WEBSITE_HOSTNAME", TEST_WEBSITE_HOSTNAME, + "WEBSITE_INSTANCE_ID", TEST_WEBSITE_INSTANCE_ID, + "WEBSITE_HOME_STAMPNAME", TEST_WEBSITE_HOME_STAMPNAME, + "WEBSITE_RESOURCE_GROUP", TEST_WEBSITE_RESOURCE_GROUP, + "WEBSITE_OWNER_NAME", TEST_WEBSITE_OWNER_NAME); + + @Test + void defaultValues() { + createResource(DEFAULT_ENV_VARS) + .containsEntry(SERVICE_NAME, TEST_WEBSITE_SITE_NAME) + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, "azure_app_service") + .containsEntry( + CLOUD_RESOURCE_ID, + "/subscriptions/TEST_WEBSITE_OWNER_NAME/resourceGroups/TEST_WEBSITE_RESOURCE_GROUP/providers/Microsoft.Web/sites/TEST_WEBSITE_SITE_NAME") + .containsEntry(CLOUD_REGION, TEST_REGION_NAME) + .containsEntry(DEPLOYMENT_ENVIRONMENT, TEST_WEBSITE_SLOT_NAME) + .containsEntry(HOST_ID, TEST_WEBSITE_HOSTNAME) + .containsEntry(SERVICE_INSTANCE_ID, TEST_WEBSITE_INSTANCE_ID) + .containsEntry( + AzureAppServiceResourceProvider.AZURE_APP_SERVICE_STAMP_RESOURCE_ATTRIBUTE, + TEST_WEBSITE_HOME_STAMPNAME); + } + + @Test + void subscriptionFromOwner() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.put("WEBSITE_OWNER_NAME", "foo+bar"); + + createResource(map) + .containsEntry( + CLOUD_RESOURCE_ID, + "/subscriptions/foo/resourceGroups/TEST_WEBSITE_RESOURCE_GROUP/providers/Microsoft.Web/sites/TEST_WEBSITE_SITE_NAME"); + } + + @Test + void noResourceId() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.remove("WEBSITE_RESOURCE_GROUP"); + + createResource(map).doesNotContainKey(CLOUD_RESOURCE_ID); + } + + @Test + void noWebsite() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.remove("WEBSITE_SITE_NAME"); + + createResource(map).isEmpty(); + } + + @Test + void isFunction() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.put("FUNCTIONS_EXTENSION_VERSION", "3.0"); + + createResource(map).isEmpty(); + } + + @NotNull + private static AttributesAssert createResource(Map map) { + return OpenTelemetryAssertions.assertThat( + new AzureAppServiceResourceProvider(map).createResource(null).getAttributes()); + } +} diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProviderTest.java new file mode 100644 index 000000000..5ac1a4be7 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureContainersResourceProviderTest.java @@ -0,0 +1,56 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_NAME; +import static io.opentelemetry.semconv.ServiceAttributes.SERVICE_VERSION; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes.SERVICE_INSTANCE_ID; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +class AzureContainersResourceProviderTest { + private static final String TEST_APP_NAME = "TEST_APP_NAME"; + private static final String TEST_REPLICA_NAME = "TEST_REPLICA_NAME"; + private static final String TEST_REVISION = "TEST_REVISION"; + + private static final ImmutableMap DEFAULT_ENV_VARS = + ImmutableMap.of( + "CONTAINER_APP_NAME", TEST_APP_NAME, + "CONTAINER_APP_REPLICA_NAME", TEST_REPLICA_NAME, + "CONTAINER_APP_REVISION", TEST_REVISION); + + @Test + void defaultValues() { + createResource(DEFAULT_ENV_VARS) + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, "azure_container_apps") + .containsEntry(SERVICE_NAME, TEST_APP_NAME) + .containsEntry(SERVICE_INSTANCE_ID, TEST_REPLICA_NAME) + .containsEntry(SERVICE_VERSION, TEST_REVISION); + } + + @Test + void isNotContainer() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.remove("CONTAINER_APP_NAME"); + + createResource(map).isEmpty(); + } + + @NotNull + private static AttributesAssert createResource(Map map) { + return OpenTelemetryAssertions.assertThat( + new AzureContainersResourceProvider(map).createResource(null).getAttributes()); + } +} diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProviderTest.java new file mode 100644 index 000000000..520e44543 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureFunctionsResourceProviderTest.java @@ -0,0 +1,61 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_INSTANCE; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_MAX_MEMORY; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_NAME; +import static io.opentelemetry.semconv.incubating.FaasIncubatingAttributes.FAAS_VERSION; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +class AzureFunctionsResourceProviderTest { + private static final String TEST_WEBSITE_SITE_NAME = "TEST_WEBSITE_SITE_NAME"; + private static final String TEST_REGION_NAME = "TEST_REGION_NAME"; + private static final String TEST_FUNCTION_VERSION = "TEST_VERSION"; + private static final String TEST_WEBSITE_INSTANCE_ID = "TEST_WEBSITE_INSTANCE_ID"; + private static final String TEST_MEM_LIMIT = "1024"; + private static final ImmutableMap DEFAULT_ENV_VARS = + ImmutableMap.of( + "WEBSITE_SITE_NAME", TEST_WEBSITE_SITE_NAME, + "REGION_NAME", TEST_REGION_NAME, + "WEBSITE_MEMORY_LIMIT_MB", TEST_MEM_LIMIT, + "FUNCTIONS_EXTENSION_VERSION", TEST_FUNCTION_VERSION, + "WEBSITE_INSTANCE_ID", TEST_WEBSITE_INSTANCE_ID); + + @Test + void defaultValues() { + createResource(DEFAULT_ENV_VARS) + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, "azure_functions") + .containsEntry(FAAS_NAME, TEST_WEBSITE_SITE_NAME) + .containsEntry(FAAS_VERSION, TEST_FUNCTION_VERSION) + .containsEntry(FAAS_INSTANCE, TEST_WEBSITE_INSTANCE_ID) + .containsEntry(FAAS_MAX_MEMORY, Long.parseLong(TEST_MEM_LIMIT)); + } + + @Test + void isNotFunction() { + HashMap map = new HashMap<>(DEFAULT_ENV_VARS); + map.remove("FUNCTIONS_EXTENSION_VERSION"); + + createResource(map).isEmpty(); + } + + @NotNull + private static AttributesAssert createResource(Map map) { + return OpenTelemetryAssertions.assertThat( + new AzureFunctionsResourceProvider(map).createResource(null).getAttributes()); + } +} diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProviderTest.java new file mode 100644 index 000000000..281416ce5 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/AzureVmResourceProviderTest.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_REGION; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_RESOURCE_ID; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_ID; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_NAME; +import static io.opentelemetry.semconv.incubating.HostIncubatingAttributes.HOST_TYPE; +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_TYPE; +import static io.opentelemetry.semconv.incubating.OsIncubatingAttributes.OS_VERSION; + +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.semconv.incubating.CloudIncubatingAttributes; +import java.util.Optional; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; + +class AzureVmResourceProviderTest extends MetadataBasedResourceProviderTest { + @NotNull + @Override + protected ResourceProvider getResourceProvider(Supplier> client) { + return new AzureVmResourceProvider(client); + } + + @Override + protected String getPlatform() { + return CloudIncubatingAttributes.CloudPlatformValues.AZURE_VM; + } + + @Override + protected void assertDefaultAttributes(AttributesAssert attributesAssert) { + attributesAssert + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, CloudIncubatingAttributes.CloudPlatformValues.AZURE_VM) + .containsEntry(CLOUD_REGION, "westus") + .containsEntry( + CLOUD_RESOURCE_ID, + "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname") + .containsEntry(HOST_ID, "02aab8a4-74ef-476e-8182-f6d2ba4166a6") + .containsEntry(HOST_NAME, "examplevmname") + .containsEntry(HOST_TYPE, "Standard_A3") + .containsEntry(OS_TYPE, "Linux") + .containsEntry(OS_VERSION, "15.05.22") + .containsEntry("azure.vm.scaleset.name", "crpteste9vflji9") + .containsEntry("azure.vm.sku", "18.04-LTS"); + } +} diff --git a/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/MetadataBasedResourceProviderTest.java b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/MetadataBasedResourceProviderTest.java new file mode 100644 index 000000000..2168af3a8 --- /dev/null +++ b/azure-resources/src/test/java/io/opentelemetry/contrib/azure/resource/MetadataBasedResourceProviderTest.java @@ -0,0 +1,109 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.azure.resource; + +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PLATFORM; +import static io.opentelemetry.semconv.incubating.CloudIncubatingAttributes.CLOUD_PROVIDER; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.common.HttpStatus; +import com.linecorp.armeria.common.MediaType; +import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.testing.assertj.AttributesAssert; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Supplier; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public abstract class MetadataBasedResourceProviderTest { + @RegisterExtension + public static final MockWebServerExtension server = new MockWebServerExtension(); + + @NotNull + private AttributesAssert mockServerResponse() { + return createResource( + () -> { + try { + return AzureMetadataService.fetchMetadata(server.httpUri().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }); + } + + @NotNull + private AttributesAssert createResource(Supplier> client) { + Resource resource = getResourceProvider(client).createResource(null); + return OpenTelemetryAssertions.assertThat(resource.getAttributes()); + } + + @NotNull + protected abstract ResourceProvider getResourceProvider(Supplier> client); + + private void assertOnlyProvider(AttributesAssert attributesAssert) { + attributesAssert + .hasSize(2) + .containsEntry(CLOUD_PROVIDER, "azure") + .containsEntry(CLOUD_PLATFORM, getPlatform()); + } + + protected abstract String getPlatform(); + + protected abstract void assertDefaultAttributes(AttributesAssert attributesAssert); + + protected static String okResponse() { + try { + return CharStreams.toString( + new InputStreamReader( + Objects.requireNonNull( + AzureVmResourceProviderTest.class + .getClassLoader() + .getResourceAsStream("response.json")), + Charsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Test + public void successFromFile() { + assertDefaultAttributes(createResource(() -> Optional.of(okResponse()))); + } + + @Test + public void successFromMockServer() { + server.enqueue(HttpResponse.of(MediaType.JSON, okResponse())); + assertDefaultAttributes(mockServerResponse()); + } + + @Test + public void responseNotFound() { + server.enqueue(HttpResponse.of(HttpStatus.NOT_FOUND)); + mockServerResponse().isEmpty(); + } + + @Test + public void responseEmpty() { + server.enqueue(HttpResponse.of("")); + assertOnlyProvider(mockServerResponse()); + } + + @Test + public void responseEmptyJson() { + server.enqueue(HttpResponse.of("{}")); + assertOnlyProvider(mockServerResponse()); + } +} diff --git a/azure-resources/src/test/resources/response.json b/azure-resources/src/test/resources/response.json new file mode 100644 index 000000000..255b41fba --- /dev/null +++ b/azure-resources/src/test/resources/response.json @@ -0,0 +1,159 @@ +{ + "compute": { + "azEnvironment": "AZUREPUBLICCLOUD", + "additionalCapabilities": { + "hibernationEnabled": "true" + }, + "hostGroup": { + "id": "testHostGroupId" + }, + "extendedLocation": { + "type": "edgeZone", + "name": "microsoftlosangeles" + }, + "evictionPolicy": "", + "isHostCompatibilityLayerVm": "true", + "licenseType": "", + "location": "westus", + "name": "examplevmname", + "offer": "UbuntuServer", + "osProfile": { + "adminUsername": "admin", + "computerName": "examplevmname", + "disablePasswordAuthentication": "true" + }, + "osType": "Linux", + "placementGroupId": "f67c14ab-e92c-408c-ae2d-da15866ec79a", + "plan": { + "name": "planName", + "product": "planProduct", + "publisher": "planPublisher" + }, + "platformFaultDomain": "36", + "platformSubFaultDomain": "", + "platformUpdateDomain": "42", + "priority": "Regular", + "publicKeys": [{ + "keyData": "ssh-rsa 0", + "path": "/home/user/.ssh/authorized_keys0" + }, + { + "keyData": "ssh-rsa 1", + "path": "/home/user/.ssh/authorized_keys1" + } + ], + "publisher": "Canonical", + "resourceGroupName": "macikgo-test-may-23", + "resourceId": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/virtualMachines/examplevmname", + "securityProfile": { + "secureBootEnabled": "true", + "virtualTpmEnabled": "false", + "encryptionAtHost": "true", + "securityType": "TrustedLaunch" + }, + "sku": "18.04-LTS", + "storageProfile": { + "dataDisks": [{ + "bytesPerSecondThrottle": "979202048", + "caching": "None", + "createOption": "Empty", + "diskCapacityBytes": "274877906944", + "diskSizeGB": "1024", + "image": { + "uri": "" + }, + "isSharedDisk": "false", + "isUltraDisk": "true", + "lun": "0", + "managedDisk": { + "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampledatadiskname", + "storageAccountType": "StandardSSD_LRS" + }, + "name": "exampledatadiskname", + "opsPerSecondThrottle": "65280", + "vhd": { + "uri": "" + }, + "writeAcceleratorEnabled": "false" + }], + "imageReference": { + "id": "", + "offer": "UbuntuServer", + "publisher": "Canonical", + "sku": "16.04.0-LTS", + "version": "latest", + "communityGalleryImageId": "/CommunityGalleries/testgallery/Images/1804Gen2/Versions/latest", + "sharedGalleryImageId": "/SharedGalleries/1P/Images/gen2/Versions/latest", + "exactVersion": "1.1686127202.30113" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "diskSizeGB": "30", + "diffDiskSettings": { + "option": "Local" + }, + "encryptionSettings": { + "enabled": "false", + "diskEncryptionKey": { + "sourceVault": { + "id": "/subscriptions/test-source-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "secretUrl": "https://test-disk.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" + }, + "keyEncryptionKey": { + "sourceVault": { + "id": "/subscriptions/test-key-guid/resourceGroups/testrg/providers/Microsoft.KeyVault/vaults/test-kv" + }, + "keyUrl": "https://test-key.vault.azure.net/secrets/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx" + } + }, + "image": { + "uri": "" + }, + "managedDisk": { + "id": "/subscriptions/xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx/resourceGroups/macikgo-test-may-23/providers/Microsoft.Compute/disks/exampleosdiskname", + "storageAccountType": "StandardSSD_LRS" + }, + "name": "exampleosdiskname", + "osType": "Linux", + "vhd": { + "uri": "" + }, + "writeAcceleratorEnabled": "false" + }, + "resourceDisk": { + "size": "4096" + } + }, + "subscriptionId": "xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx", + "tags": "baz:bash;foo:bar", + "version": "15.05.22", + "virtualMachineScaleSet": { + "id": "/subscriptions/xxxxxxxx-xxxxx-xxx-xxx-xxxx/resourceGroups/resource-group-name/providers/Microsoft.Compute/virtualMachineScaleSets/virtual-machine-scale-set-name" + }, + "vmId": "02aab8a4-74ef-476e-8182-f6d2ba4166a6", + "vmScaleSetName": "crpteste9vflji9", + "vmSize": "Standard_A3", + "zone": "" + }, + "network": { + "interface": [{ + "ipv4": { + "ipAddress": [{ + "privateIpAddress": "10.144.133.132", + "publicIpAddress": "" + }], + "subnet": [{ + "address": "10.144.133.128", + "prefix": "26" + }] + }, + "ipv6": { + "ipAddress": [ + ] + }, + "macAddress": "0011AAFFBB22" + }] + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 882cd2cb9..d0fabf6db 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -61,6 +61,7 @@ include(":all") include(":aws-resources") include(":aws-xray") include(":aws-xray-propagator") +include(":azure-resources") include(":baggage-processor") include(":compressors:compressor-zstd") include(":consistent-sampling")