diff --git a/.github/component_owners.yml b/.github/component_owners.yml index c34a4974..ad0c038c 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -10,6 +10,8 @@ # NOTE when adding/updating one of the component names, don't forget to update the associated # `comp:*` labels components: + aws-resources: + - willarmiros aws-xray: - willarmiros consistent-sampling: diff --git a/aws-resources/README.md b/aws-resources/README.md new file mode 100644 index 00000000..90efc7c9 --- /dev/null +++ b/aws-resources/README.md @@ -0,0 +1,9 @@ +# OpenTelemetry AWS Resource Support + +This module contains AWS resource detectors including Beanstalk, EC2, ECS, EKS, and Lambda. + +## Component owners + +- [William Armiros](https://github.com/willarmiros), AWS + +Learn more about component owners in [component_owners.yml](../.github/component_owners.yml). diff --git a/aws-resources/build.gradle.kts b/aws-resources/build.gradle.kts new file mode 100644 index 00000000..1c85263a --- /dev/null +++ b/aws-resources/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("otel.java-conventions") + + // TODO: uncomment after 1.18.0 release + // id("otel.publish-conventions") +} + +description = "OpenTelemetry AWS Resources Support" + +dependencies { + api("io.opentelemetry:opentelemetry-api") + api("io.opentelemetry:opentelemetry-sdk") + + implementation("io.opentelemetry:opentelemetry-semconv") + + 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("com.linecorp.armeria:armeria-junit5") + testRuntimeOnly("org.bouncycastle:bcpkix-jdk15on") + testImplementation("com.google.guava:guava") + testImplementation("org.skyscreamer:jsonassert") +} diff --git a/aws-resources/gradle.properties b/aws-resources/gradle.properties new file mode 100644 index 00000000..a0402e1e --- /dev/null +++ b/aws-resources/gradle.properties @@ -0,0 +1,2 @@ +# TODO: uncomment when ready to mark as stable +# otel.stable=true diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResource.java new file mode 100644 index 00000000..17f98167 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResource.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current EC2 instance if + * running on AWS Elastic Beanstalk. + */ +public final class BeanstalkResource { + + private static final Logger logger = Logger.getLogger(BeanstalkResource.class.getName()); + + private static final String DEVELOPMENT_ID = "deployment_id"; + private static final String VERSION_LABEL = "version_label"; + private static final String ENVIRONMENT_NAME = "environment_name"; + private static final String BEANSTALK_CONF_PATH = "/var/elasticbeanstalk/xray/environment.conf"; + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current EC2 + * instance if running on AWS Elastic Beanstalk. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(BEANSTALK_CONF_PATH); + } + + // Visible for testing + static Resource buildResource(String configPath) { + File configFile = new File(configPath); + if (!configFile.exists()) { + return Resource.empty(); + } + + AttributesBuilder attrBuilders = Attributes.builder(); + try (JsonParser parser = JSON_FACTORY.createParser(configFile)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + logger.log(Level.WARNING, "Invalid Beanstalk config: ", configPath); + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + parser.nextValue(); + String value = parser.getText(); + switch (parser.getCurrentName()) { + case DEVELOPMENT_ID: + attrBuilders.put(ResourceAttributes.SERVICE_INSTANCE_ID, value); + break; + case VERSION_LABEL: + attrBuilders.put(ResourceAttributes.SERVICE_VERSION, value); + break; + case ENVIRONMENT_NAME: + attrBuilders.put(ResourceAttributes.SERVICE_NAMESPACE, value); + break; + default: + parser.skipChildren(); + } + } + } catch (IOException e) { + logger.log(Level.WARNING, "Could not parse Beanstalk config.", e); + return Resource.empty(); + } + + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + attrBuilders.put( + ResourceAttributes.CLOUD_PLATFORM, + ResourceAttributes.CloudPlatformValues.AWS_ELASTIC_BEANSTALK); + + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); + } + + private BeanstalkResource() {} +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceProvider.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceProvider.java new file mode 100644 index 00000000..f1095946 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link BeanstalkResource}. */ +public final class BeanstalkResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return BeanstalkResource.get(); + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/DockerHelper.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/DockerHelper.java new file mode 100644 index 00000000..5b8aefea --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/DockerHelper.java @@ -0,0 +1,54 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +class DockerHelper { + + private static final Logger logger = Logger.getLogger(DockerHelper.class.getName()); + private static final int CONTAINER_ID_LENGTH = 64; + private static final String DEFAULT_CGROUP_PATH = "/proc/self/cgroup"; + + private final String cgroupPath; + + DockerHelper() { + this(DEFAULT_CGROUP_PATH); + } + + // Visible for testing + DockerHelper(String cgroupPath) { + this.cgroupPath = cgroupPath; + } + + /** + * Get docker container id from local cgroup file. + * + * @return docker container ID. Empty string if it can`t be found. + */ + @SuppressWarnings("DefaultCharset") + public String getContainerId() { + try (BufferedReader br = new BufferedReader(new FileReader(cgroupPath))) { + String line; + while ((line = br.readLine()) != null) { + if (line.length() > CONTAINER_ID_LENGTH) { + return line.substring(line.length() - CONTAINER_ID_LENGTH); + } + } + } catch (FileNotFoundException e) { + logger.log(Level.WARNING, "Failed to read container id, cgroup file does not exist."); + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to read container id: " + e.getMessage()); + } + + return ""; + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java new file mode 100644 index 00000000..24f1b336 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2Resource.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current EC2 instance if + * running on AWS EC2. + */ +public final class Ec2Resource { + + private static final Logger logger = Logger.getLogger(Ec2Resource.class.getName()); + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + private static final String DEFAULT_IMDS_ENDPOINT = "169.254.169.254"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a @link Resource} which provides information about the current EC2 instance if running + * on AWS EC2. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + // This property is only for testing e.g., with a mock IMDS server and never in production so we + // just + // read from a system property. This is similar to the AWS SDK. + return buildResource( + System.getProperty("otel.aws.imds.endpointOverride", DEFAULT_IMDS_ENDPOINT)); + } + + // Visible for testing + static Resource buildResource(String endpoint) { + String urlBase = "http://" + endpoint; + URL identityDocumentUrl; + URL hostnameUrl; + URL tokenUrl; + try { + identityDocumentUrl = new URL(urlBase + "/latest/dynamic/instance-identity/document"); + hostnameUrl = new URL(urlBase + "/latest/meta-data/hostname"); + tokenUrl = new URL(urlBase + "/latest/api/token"); + } catch (MalformedURLException e) { + // Can only happen when overriding the endpoint in testing so just throw. + throw new IllegalArgumentException("Illegal endpoint: " + endpoint, e); + } + + String token = fetchToken(tokenUrl); + + // If token is empty, either IMDSv2 isn't enabled or an unexpected failure happened. We can + // still get data if IMDSv1 is enabled. + String identity = fetchIdentity(identityDocumentUrl, token); + if (identity.isEmpty()) { + // If no identity document, assume we are not actually running on EC2. + return Resource.empty(); + } + + String hostname = fetchHostname(hostnameUrl, token); + + AttributesBuilder attrBuilders = Attributes.builder(); + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + attrBuilders.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_EC2); + + try (JsonParser parser = JSON_FACTORY.createParser(identity)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + throw new IOException("Invalid JSON:" + identity); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + switch (parser.getCurrentName()) { + case "instanceId": + attrBuilders.put(ResourceAttributes.HOST_ID, value); + break; + case "availabilityZone": + attrBuilders.put(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, value); + break; + case "instanceType": + attrBuilders.put(ResourceAttributes.HOST_TYPE, value); + break; + case "imageId": + attrBuilders.put(ResourceAttributes.HOST_IMAGE_ID, value); + break; + case "accountId": + attrBuilders.put(ResourceAttributes.CLOUD_ACCOUNT_ID, value); + break; + case "region": + attrBuilders.put(ResourceAttributes.CLOUD_REGION, value); + break; + default: + parser.skipChildren(); + } + } + } catch (IOException e) { + logger.log(Level.WARNING, "Could not parse identity document, resource not filled.", e); + return Resource.empty(); + } + + attrBuilders.put(ResourceAttributes.HOST_NAME, hostname); + + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); + } + + private static String fetchToken(URL tokenUrl) { + return fetchString("PUT", tokenUrl, "", /* includeTtl= */ true); + } + + private static String fetchIdentity(URL identityDocumentUrl, String token) { + return fetchString("GET", identityDocumentUrl, token, /* includeTtl= */ false); + } + + private static String fetchHostname(URL hostnameUrl, String token) { + return fetchString("GET", hostnameUrl, token, /* includeTtl= */ false); + } + + // Generic HTTP fetch function for IMDS. + private static String fetchString(String httpMethod, URL url, String token, boolean includeTtl) { + SimpleHttpClient client = new SimpleHttpClient(); + Map headers = new HashMap<>(); + + if (includeTtl) { + headers.put("X-aws-ec2-metadata-token-ttl-seconds", "60"); + } + if (!token.isEmpty()) { + headers.put("X-aws-ec2-metadata-token", token); + } + + return client.fetchString(httpMethod, url.toString(), headers, null); + } + + private Ec2Resource() {} +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceProvider.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceProvider.java new file mode 100644 index 00000000..b1798f72 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link Ec2Resource}. */ +public final class Ec2ResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return Ec2Resource.get(); + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java new file mode 100644 index 00000000..d6c296c2 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResource.java @@ -0,0 +1,266 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +/** + * A factory for a {@link Resource} which provides information about the current ECS container if + * running on AWS ECS. + */ +public final class EcsResource { + private static final Logger logger = Logger.getLogger(EcsResource.class.getName()); + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4"; + private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current ECS + * container if running on AWS ECS. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(System.getenv(), new SimpleHttpClient()); + } + + // Visible for testing + static Resource buildResource(Map sysEnv, SimpleHttpClient httpClient) { + // Note: If V4 is set V3 is set as well, so check V4 first. + String ecsMetadataUrl = + sysEnv.getOrDefault(ECS_METADATA_KEY_V4, sysEnv.getOrDefault(ECS_METADATA_KEY_V3, "")); + if (!ecsMetadataUrl.isEmpty()) { + AttributesBuilder attrBuilders = Attributes.builder(); + fetchMetadata(httpClient, ecsMetadataUrl, attrBuilders); + // For TaskARN, Family, Revision. + // May put the same attribute twice but that shouldn't matter. + fetchMetadata(httpClient, ecsMetadataUrl + "/task", attrBuilders); + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); + } + // Not running on ECS + return Resource.empty(); + } + + static void fetchMetadata( + SimpleHttpClient httpClient, String url, AttributesBuilder attrBuilders) { + String json = httpClient.fetchString("GET", url, Collections.emptyMap(), null); + if (json.isEmpty()) { + return; + } + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + attrBuilders.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_ECS); + try (JsonParser parser = JSON_FACTORY.createParser(json)) { + parser.nextToken(); + LogArnBuilder logArnBuilder = new LogArnBuilder(); + parseResponse(parser, attrBuilders, logArnBuilder); + + logArnBuilder + .getLogGroupArn() + .ifPresent( + logGroupArn -> { + attrBuilders.put( + ResourceAttributes.AWS_LOG_GROUP_ARNS, Collections.singletonList(logGroupArn)); + }); + + logArnBuilder + .getLogStreamArn() + .ifPresent( + logStreamArn -> { + attrBuilders.put( + ResourceAttributes.AWS_LOG_STREAM_ARNS, + Collections.singletonList(logStreamArn)); + }); + } catch (IOException e) { + logger.log(Level.WARNING, "Can't get ECS metadata", e); + } + } + + static void parseResponse( + JsonParser parser, AttributesBuilder attrBuilders, LogArnBuilder logArnBuilder) + throws IOException { + if (!parser.isExpectedStartObjectToken()) { + logger.log(Level.WARNING, "Couldn't parse ECS metadata, invalid JSON"); + return; + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + switch (parser.getCurrentName()) { + case "DockerId": + attrBuilders.put(ResourceAttributes.CONTAINER_ID, value); + break; + case "DockerName": + attrBuilders.put(ResourceAttributes.CONTAINER_NAME, value); + break; + case "ContainerARN": + attrBuilders.put(ResourceAttributes.AWS_ECS_CONTAINER_ARN, value); + logArnBuilder.setContainerArn(value); + break; + case "Image": + DockerImage parsedImage = DockerImage.parse(value); + if (parsedImage != null) { + attrBuilders.put(ResourceAttributes.CONTAINER_IMAGE_NAME, parsedImage.getRepository()); + attrBuilders.put(ResourceAttributes.CONTAINER_IMAGE_TAG, parsedImage.getTag()); + } + break; + case "ImageID": + attrBuilders.put("aws.ecs.container.image.id", value); + break; + case "LogOptions": + // Recursively parse LogOptions + parseResponse(parser, attrBuilders, logArnBuilder); + break; + case "awslogs-group": + attrBuilders.put(ResourceAttributes.AWS_LOG_GROUP_NAMES, value); + logArnBuilder.setLogGroupName(value); + break; + case "awslogs-stream": + attrBuilders.put(ResourceAttributes.AWS_LOG_STREAM_NAMES, value); + logArnBuilder.setLogStreamName(value); + break; + case "awslogs-region": + logArnBuilder.setRegion(value); + break; + case "TaskARN": + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_ARN, value); + break; + case "LaunchType": + attrBuilders.put(ResourceAttributes.AWS_ECS_LAUNCHTYPE, value.toLowerCase()); + break; + case "Family": + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_FAMILY, value); + break; + case "Revision": + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_REVISION, value); + break; + default: + parser.skipChildren(); + break; + } + } + } + + private EcsResource() {} + + /** + * This builder can piece together the ARN of a log group or a log stream from region, account, + * group name and stream name as the ARN isn't part of the ECS metadata. + * + *

If we just set AWS_LOG_GROUP_NAMES then the CloudWatch X-Ray traces view displays "An error + * occurred fetching your data". That's why it's important we set the ARN. + */ + private static class LogArnBuilder { + + @Nullable String region; + @Nullable String account; + @Nullable String logGroupName; + @Nullable String logStreamName; + + void setRegion(@Nullable String region) { + this.region = region; + } + + void setLogGroupName(@Nullable String logGroupName) { + this.logGroupName = logGroupName; + } + + void setLogStreamName(@Nullable String logStreamName) { + this.logStreamName = logStreamName; + } + + void setContainerArn(@Nullable String containerArn) { + if (containerArn != null) { + account = containerArn.split(":")[4]; + } + } + + Optional getLogGroupArn() { + if (region == null || account == null || logGroupName == null) { + return Optional.empty(); + } + + return Optional.of("arn:aws:logs:" + region + ":" + account + ":log-group:" + logGroupName); + } + + Optional getLogStreamArn() { + if (region == null || account == null || logGroupName == null || logStreamName == null) { + return Optional.empty(); + } + + return Optional.of( + "arn:aws:logs:" + + region + + ":" + + account + + ":log-group:" + + logGroupName + + ":log-stream:" + + logStreamName); + } + } + + /** This can parse a Docker image name into its parts: repository, tag and sha256. */ + private static class DockerImage { + + private static final Pattern imagePattern = + Pattern.compile( + "^(?([^/\\s]+/)?([^:\\s]+))(:(?[^@\\s]+))?(@sha256:(?\\d+))?$"); + + final String repository; + final String tag; + + private DockerImage(String repository, String tag) { + this.repository = repository; + this.tag = tag; + } + + String getRepository() { + return repository; + } + + String getTag() { + return tag; + } + + @Nullable + static DockerImage parse(@Nullable String image) { + if (image == null || image.isEmpty()) { + return null; + } + Matcher matcher = imagePattern.matcher(image); + if (!matcher.matches()) { + logger.log(Level.WARNING, "Couldn't parse image '" + image + "'"); + return null; + } + String repository = matcher.group("repository"); + String tag = matcher.group("tag"); + if (tag == null || tag.isEmpty()) { + tag = "latest"; + } + return new DockerImage(repository, tag); + } + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResourceProvider.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResourceProvider.java new file mode 100644 index 00000000..be07b0f6 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EcsResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link EcsResource}. */ +public final class EcsResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return EcsResource.get(); + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java new file mode 100644 index 00000000..fdb4351f --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResource.java @@ -0,0 +1,159 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A factory for a {@link Resource} which provides information about the current ECS container if + * running on AWS EKS. + */ +public final class EksResource { + private static final Logger logger = Logger.getLogger(EksResource.class.getName()); + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + + static final String K8S_SVC_URL = "https://kubernetes.default.svc"; + static final String AUTH_CONFIGMAP_PATH = "/api/v1/namespaces/kube-system/configmaps/aws-auth"; + static final String CW_CONFIGMAP_PATH = + "/api/v1/namespaces/amazon-cloudwatch/configmaps/cluster-info"; + private static final String K8S_TOKEN_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/token"; + private static final String K8S_CERT_PATH = + "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"; + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the current ECS + * container if running on AWS EKS. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(new SimpleHttpClient(), new DockerHelper(), K8S_TOKEN_PATH, K8S_CERT_PATH); + } + + // Visible for testing + static Resource buildResource( + SimpleHttpClient httpClient, + DockerHelper dockerHelper, + String k8sTokenPath, + String k8sKeystorePath) { + if (!isEks(k8sTokenPath, k8sKeystorePath, httpClient)) { + return Resource.empty(); + } + + AttributesBuilder attrBuilders = Attributes.builder(); + attrBuilders.put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + attrBuilders.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_EKS); + + String clusterName = getClusterName(httpClient); + if (clusterName != null && !clusterName.isEmpty()) { + attrBuilders.put(ResourceAttributes.K8S_CLUSTER_NAME, clusterName); + } + + String containerId = dockerHelper.getContainerId(); + if (containerId != null && !containerId.isEmpty()) { + attrBuilders.put(ResourceAttributes.CONTAINER_ID, containerId); + } + + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); + } + + private static boolean isEks( + String k8sTokenPath, String k8sKeystorePath, SimpleHttpClient httpClient) { + if (!isK8s(k8sTokenPath, k8sKeystorePath)) { + logger.log(Level.FINE, "Not running on k8s."); + return false; + } + + Map requestProperties = new HashMap<>(); + requestProperties.put("Authorization", getK8sCredHeader()); + String awsAuth = + httpClient.fetchString( + "GET", K8S_SVC_URL + AUTH_CONFIGMAP_PATH, requestProperties, K8S_CERT_PATH); + + return awsAuth != null && !awsAuth.isEmpty(); + } + + private static boolean isK8s(String k8sTokenPath, String k8sKeystorePath) { + File k8sTokeyFile = new File(k8sTokenPath); + File k8sKeystoreFile = new File(k8sKeystorePath); + return k8sTokeyFile.exists() && k8sKeystoreFile.exists(); + } + + private static String getClusterName(SimpleHttpClient httpClient) { + Map requestProperties = new HashMap<>(); + requestProperties.put("Authorization", getK8sCredHeader()); + String json = + httpClient.fetchString( + "GET", K8S_SVC_URL + CW_CONFIGMAP_PATH, requestProperties, K8S_CERT_PATH); + + try (JsonParser parser = JSON_FACTORY.createParser(json)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + throw new IOException("Invalid JSON:" + json); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + parser.nextToken(); + if (!parser.getCurrentName().equals("data")) { + parser.skipChildren(); + continue; + } + + if (!parser.isExpectedStartObjectToken()) { + throw new IOException("Invalid JSON:" + json); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + if (!parser.getCurrentName().equals("cluster.name")) { + parser.skipChildren(); + continue; + } + return value; + } + } + } catch (IOException e) { + logger.log(Level.WARNING, "Can't get cluster name on EKS.", e); + } + return ""; + } + + private static String getK8sCredHeader() { + try { + String content = + new String(Files.readAllBytes(Paths.get(K8S_TOKEN_PATH)), StandardCharsets.UTF_8); + return "Bearer " + content; + } catch (IOException e) { + logger.log(Level.WARNING, "Unable to load K8s client token.", e); + } + return ""; + } + + private EksResource() {} +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResourceProvider.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResourceProvider.java new file mode 100644 index 00000000..13fef7f0 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/EksResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link EksResource}. */ +public final class EksResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return EksResource.get(); + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResource.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResource.java new file mode 100644 index 00000000..a3c16ba5 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResource.java @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.Map; +import java.util.stream.Stream; + +/** A factory for a {@link Resource} which provides information about the AWS Lambda function. */ +public final class LambdaResource { + + private static final Resource INSTANCE = buildResource(); + + /** + * Returns a factory for a {@link Resource} which provides information about the AWS Lambda + * function. + */ + public static Resource get() { + return INSTANCE; + } + + private static Resource buildResource() { + return buildResource(System.getenv()); + } + + // Visible for testing + static Resource buildResource(Map environmentVariables) { + String region = environmentVariables.getOrDefault("AWS_REGION", ""); + String functionName = environmentVariables.getOrDefault("AWS_LAMBDA_FUNCTION_NAME", ""); + String functionVersion = environmentVariables.getOrDefault("AWS_LAMBDA_FUNCTION_VERSION", ""); + + if (!isLambda(functionName, functionVersion)) { + return Resource.empty(); + } + + AttributesBuilder builder = + Attributes.builder() + .put(ResourceAttributes.CLOUD_PROVIDER, ResourceAttributes.CloudProviderValues.AWS); + builder.put( + ResourceAttributes.CLOUD_PLATFORM, ResourceAttributes.CloudPlatformValues.AWS_LAMBDA); + + if (!region.isEmpty()) { + builder.put(ResourceAttributes.CLOUD_REGION, region); + } + if (!functionName.isEmpty()) { + builder.put(ResourceAttributes.FAAS_NAME, functionName); + } + if (!functionVersion.isEmpty()) { + builder.put(ResourceAttributes.FAAS_VERSION, functionVersion); + } + + return Resource.create(builder.build(), ResourceAttributes.SCHEMA_URL); + } + + private static boolean isLambda(String... envVariables) { + return Stream.of(envVariables).anyMatch(v -> !v.isEmpty()); + } + + private LambdaResource() {} +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResourceProvider.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResourceProvider.java new file mode 100644 index 00000000..ac6e230b --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/LambdaResourceProvider.java @@ -0,0 +1,18 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; + +/** {@link ResourceProvider} for automatically configuring {@link LambdaResource}. */ +public final class LambdaResourceProvider implements ResourceProvider { + @Override + public Resource createResource(ConfigProperties config) { + return LambdaResource.get(); + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java new file mode 100644 index 00000000..12bc6e34 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClient.java @@ -0,0 +1,145 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.time.Duration; +import java.util.Collection; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; + +/** A simple HTTP client based on OkHttp. Not meant for high throughput. */ +final class SimpleHttpClient { + + private static final Logger logger = Logger.getLogger(SimpleHttpClient.class.getName()); + + private static final Duration TIMEOUT = Duration.ofSeconds(2); + + private static final RequestBody EMPTY_BODY = RequestBody.create(new byte[0]); + + /** Fetch a string from a remote server. */ + public String fetchString( + String httpMethod, String urlStr, Map headers, @Nullable String certPath) { + + OkHttpClient.Builder clientBuilder = + new OkHttpClient.Builder() + .callTimeout(TIMEOUT) + .connectTimeout(TIMEOUT) + .readTimeout(TIMEOUT); + + if (urlStr.startsWith("https") && certPath != null) { + KeyStore keyStore = getKeystoreForTrustedCert(certPath); + X509TrustManager trustManager = buildTrustManager(keyStore); + SSLSocketFactory socketFactory = buildSslSocketFactory(trustManager); + if (socketFactory != null) { + clientBuilder.sslSocketFactory(socketFactory, trustManager); + } + } + + OkHttpClient client = clientBuilder.build(); + + // AWS incorrectly uses PUT despite having no request body, OkHttp will only allow us to send + // GET with null body or PUT with empty string body + RequestBody requestBody = null; + if (httpMethod.equals("PUT")) { + requestBody = EMPTY_BODY; + } + Request.Builder requestBuilder = + new Request.Builder().url(urlStr).method(httpMethod, requestBody); + + headers.forEach(requestBuilder::addHeader); + + try (Response response = client.newCall(requestBuilder.build()).execute()) { + int responseCode = response.code(); + if (responseCode != 200) { + logger.log( + Level.FINE, + "Error response from " + + urlStr + + " code (" + + responseCode + + ") text " + + response.message()); + return ""; + } + ResponseBody body = response.body(); + return body != null ? body.string() : ""; + } catch (IOException e) { + logger.log(Level.FINE, "SimpleHttpClient fetch string failed.", e); + } + + return ""; + } + + @Nullable + private static X509TrustManager buildTrustManager(@Nullable KeyStore keyStore) { + if (keyStore == null) { + return null; + } + try { + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init(keyStore); + return (X509TrustManager) tmf.getTrustManagers()[0]; + } catch (Exception e) { + logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); + return null; + } + } + + @Nullable + private static SSLSocketFactory buildSslSocketFactory(@Nullable TrustManager trustManager) { + if (trustManager == null) { + return null; + } + try { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] {trustManager}, null); + return context.getSocketFactory(); + + } catch (Exception e) { + logger.log(Level.WARNING, "Build SslSocketFactory for K8s restful client exception.", e); + } + return null; + } + + @Nullable + private static KeyStore getKeystoreForTrustedCert(String certPath) { + try (FileInputStream fis = new FileInputStream(certPath)) { + KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); + trustStore.load(null, null); + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + + Collection certificates = certificateFactory.generateCertificates(fis); + + int i = 0; + for (Certificate certificate : certificates) { + trustStore.setCertificateEntry("cert_" + i, certificate); + i++; + } + return trustStore; + } catch (Exception e) { + logger.log(Level.WARNING, "Cannot load KeyStore from " + certPath); + return null; + } + } +} diff --git a/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/package-info.java b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/package-info.java new file mode 100644 index 00000000..c8f4ac73 --- /dev/null +++ b/aws-resources/src/main/java/io/opentelemetry/contrib/aws/resource/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * {@link io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider} implementations for inferring + * resource information for AWS services. + */ +@ParametersAreNonnullByDefault +package io.opentelemetry.contrib.aws.resource; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/aws-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider b/aws-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider new file mode 100644 index 00000000..e581f09d --- /dev/null +++ b/aws-resources/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider @@ -0,0 +1,5 @@ +io.opentelemetry.contrib.aws.resource.BeanstalkResourceProvider +io.opentelemetry.contrib.aws.resource.Ec2ResourceProvider +io.opentelemetry.contrib.aws.resource.EcsResourceProvider +io.opentelemetry.contrib.aws.resource.EksResourceProvider +io.opentelemetry.contrib.aws.resource.LambdaResourceProvider diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceTest.java new file mode 100644 index 00000000..36c6adef --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/BeanstalkResourceTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class BeanstalkResourceTest { + + @Test + void testCreateAttributes(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "beanstalk.config"); + String content = + "{\"noise\": \"noise\", \"deployment_id\":4,\"" + + "version_label\":\"2\",\"environment_name\":\"HttpSubscriber-env\"}"; + Files.write(content.getBytes(Charsets.UTF_8), file); + Resource resource = BeanstalkResource.buildResource(file.getPath()); + Attributes attributes = resource.getAttributes(); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_elastic_beanstalk"), + entry(ResourceAttributes.SERVICE_INSTANCE_ID, "4"), + entry(ResourceAttributes.SERVICE_VERSION, "2"), + entry(ResourceAttributes.SERVICE_NAMESPACE, "HttpSubscriber-env")); + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + } + + @Test + void testConfigFileMissing() { + Attributes attributes = + BeanstalkResource.buildResource("a_file_never_existing").getAttributes(); + assertThat(attributes).isEmpty(); + } + + @Test + void testBadConfigFile(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "beanstalk.config"); + String content = + "\"deployment_id\":4,\"version_label\":\"2\",\"" + + "environment_name\":\"HttpSubscriber-env\"}"; + Files.write(content.getBytes(Charsets.UTF_8), file); + Attributes attributes = BeanstalkResource.buildResource(file.getPath()).getAttributes(); + assertThat(attributes).isEmpty(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(BeanstalkResourceProvider.class::isInstance); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/DockerHelperTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/DockerHelperTest.java new file mode 100644 index 00000000..9d7ef46b --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/DockerHelperTest.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class DockerHelperTest { + + @Test + void testCgroupFileMissing() { + DockerHelper dockerHelper = new DockerHelper("a_file_never_existing"); + assertThat(dockerHelper.getContainerId()).isEmpty(); + } + + @Test + void testContainerIdMissing(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "no_container_id"); + String content = "13:pids:/\n" + "12:hugetlb:/\n" + "11:net_prio:/"; + Files.write(content.getBytes(Charsets.UTF_8), file); + + DockerHelper dockerHelper = new DockerHelper(file.getPath()); + assertThat(dockerHelper.getContainerId()).isEmpty(); + } + + @Test + void testGetContainerId(@TempDir File tempFolder) throws IOException { + File file = new File(tempFolder, "cgroup"); + String expected = "386a1920640799b5bf5a39bd94e489e5159a88677d96ca822ce7c433ff350163"; + String content = "dummy\n11:devices:/ecs/bbc36dd0-5ee0-4007-ba96-c590e0b278d2/" + expected; + Files.write(content.getBytes(Charsets.UTF_8), file); + + DockerHelper dockerHelper = new DockerHelper(file.getPath()); + assertThat(dockerHelper.getContainerId()).isEqualTo(expected); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceTest.java new file mode 100644 index 00000000..64f5d2c0 --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/Ec2ResourceTest.java @@ -0,0 +1,139 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import com.linecorp.armeria.common.AggregatedHttpRequest; +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.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class Ec2ResourceTest { + + // From https://docs.amazonaws.cn/en_us/AWSEC2/latest/UserGuide/instance-identity-documents.html + private static final String IDENTITY_DOCUMENT = + "{\n" + + " \"devpayProductCodes\" : null,\n" + + " \"marketplaceProductCodes\" : [ \"1abc2defghijklm3nopqrs4tu\" ], \n" + + " \"availabilityZone\" : \"us-west-2b\",\n" + + " \"privateIp\" : \"10.158.112.84\",\n" + + " \"version\" : \"2017-09-30\",\n" + + " \"instanceId\" : \"i-1234567890abcdef0\",\n" + + " \"billingProducts\" : null,\n" + + " \"instanceType\" : \"t2.micro\",\n" + + " \"accountId\" : \"123456789012\",\n" + + " \"imageId\" : \"ami-5fb8c835\",\n" + + " \"pendingTime\" : \"2016-11-19T16:32:11Z\",\n" + + " \"architecture\" : \"x86_64\",\n" + + " \"kernelId\" : null,\n" + + " \"ramdiskId\" : null,\n" + + " \"region\" : \"us-west-2\"\n" + + "}"; + + @RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension(); + + @Test + void imdsv2() { + server.enqueue(HttpResponse.of("token")); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, IDENTITY_DOCUMENT)); + server.enqueue(HttpResponse.of("ec2-1-2-3-4")); + + Resource resource = Ec2Resource.buildResource("localhost:" + server.httpPort()); + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + Attributes attributes = resource.getAttributes(); + + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_ec2"), + entry(ResourceAttributes.HOST_ID, "i-1234567890abcdef0"), + entry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "us-west-2b"), + entry(ResourceAttributes.HOST_TYPE, "t2.micro"), + entry(ResourceAttributes.HOST_IMAGE_ID, "ami-5fb8c835"), + entry(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789012"), + entry(ResourceAttributes.CLOUD_REGION, "us-west-2"), + entry(ResourceAttributes.HOST_NAME, "ec2-1-2-3-4")); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isEqualTo("token"); + + AggregatedHttpRequest request3 = server.takeRequest().request(); + assertThat(request3.path()).isEqualTo("/latest/meta-data/hostname"); + assertThat(request3.headers().get("X-aws-ec2-metadata-token")).isEqualTo("token"); + } + + @Test + void imdsv1() { + server.enqueue(HttpResponse.of(HttpStatus.NOT_FOUND)); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, IDENTITY_DOCUMENT)); + server.enqueue(HttpResponse.of("ec2-1-2-3-4")); + + Resource resource = Ec2Resource.buildResource("localhost:" + server.httpPort()); + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + Attributes attributes = resource.getAttributes(); + + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_ec2"), + entry(ResourceAttributes.HOST_ID, "i-1234567890abcdef0"), + entry(ResourceAttributes.CLOUD_AVAILABILITY_ZONE, "us-west-2b"), + entry(ResourceAttributes.HOST_TYPE, "t2.micro"), + entry(ResourceAttributes.HOST_IMAGE_ID, "ami-5fb8c835"), + entry(ResourceAttributes.CLOUD_ACCOUNT_ID, "123456789012"), + entry(ResourceAttributes.CLOUD_REGION, "us-west-2"), + entry(ResourceAttributes.HOST_NAME, "ec2-1-2-3-4")); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isNull(); + } + + @Test + void badJson() { + server.enqueue(HttpResponse.of(HttpStatus.NOT_FOUND)); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, "I'm not JSON")); + + Attributes attributes = + Ec2Resource.buildResource("localhost:" + server.httpPort()).getAttributes(); + assertThat(attributes).isEmpty(); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/latest/api/token"); + assertThat(request1.headers().get("X-aws-ec2-metadata-token-ttl-seconds")).isEqualTo("60"); + + AggregatedHttpRequest request2 = server.takeRequest().request(); + assertThat(request2.path()).isEqualTo("/latest/dynamic/instance-identity/document"); + assertThat(request2.headers().get("X-aws-ec2-metadata-token")).isNull(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(Ec2ResourceProvider.class::isInstance); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java new file mode 100644 index 00000000..16c26f4a --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EcsResourceTest.java @@ -0,0 +1,143 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.Mockito.when; + +import com.google.common.io.Resources; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class EcsResourceTest { + private static final String ECS_METADATA_KEY_V4 = "ECS_CONTAINER_METADATA_URI_V4"; + private static final String ECS_METADATA_KEY_V3 = "ECS_CONTAINER_METADATA_URI"; + + @Mock private SimpleHttpClient mockHttpClient; + + @Test + void testCreateAttributesV3() throws IOException { + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V3, "ecs_metadata_v3_uri"); + when(mockHttpClient.fetchString("GET", "ecs_metadata_v3_uri", Collections.emptyMap(), null)) + .thenReturn(readResourceJson("ecs-container-metadata-v3.json")); + when(mockHttpClient.fetchString( + "GET", "ecs_metadata_v3_uri/task", Collections.emptyMap(), null)) + .thenReturn(readResourceJson("ecs-task-metadata-v3.json")); + + Resource resource = EcsResource.buildResource(mockSysEnv, mockHttpClient); + Attributes attributes = resource.getAttributes(); + + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_ecs"), + entry(ResourceAttributes.CONTAINER_NAME, "ecs-nginx-5-nginx-curl-ccccb9f49db0dfe0d901"), + entry( + ResourceAttributes.CONTAINER_ID, + "43481a6ce4842eec8fe72fc28500c6b52edcc0917f105b83379f88cac1ff3946"), + entry(ResourceAttributes.CONTAINER_IMAGE_NAME, "nrdlngr/nginx-curl"), + entry(ResourceAttributes.CONTAINER_IMAGE_TAG, "latest"), + entry( + AttributeKey.stringKey("aws.ecs.container.image.id"), + "sha256:2e00ae64383cfc865ba0a2ba37f61b50a120d2d9378559dcd458dc0de47bc165"), + entry( + ResourceAttributes.AWS_ECS_TASK_ARN, + "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3"), + entry(ResourceAttributes.AWS_ECS_TASK_FAMILY, "nginx"), + entry(ResourceAttributes.AWS_ECS_TASK_REVISION, "5")); + } + + @Test + void testCreateAttributesV4() throws IOException { + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V4, "ecs_metadata_v4_uri"); + when(mockHttpClient.fetchString("GET", "ecs_metadata_v4_uri", Collections.emptyMap(), null)) + .thenReturn(readResourceJson("ecs-container-metadata-v4.json")); + when(mockHttpClient.fetchString( + "GET", "ecs_metadata_v4_uri/task", Collections.emptyMap(), null)) + .thenReturn(readResourceJson("ecs-task-metadata-v4.json")); + + Resource resource = EcsResource.buildResource(mockSysEnv, mockHttpClient); + Attributes attributes = resource.getAttributes(); + + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_ecs"), + entry(ResourceAttributes.CONTAINER_NAME, "ecs-curltest-26-curl-cca48e8dcadd97805600"), + entry( + ResourceAttributes.CONTAINER_ID, + "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66"), + entry( + ResourceAttributes.CONTAINER_IMAGE_NAME, + "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest"), + entry(ResourceAttributes.CONTAINER_IMAGE_TAG, "latest"), + entry( + AttributeKey.stringKey("aws.ecs.container.image.id"), + "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553"), + entry( + ResourceAttributes.AWS_ECS_CONTAINER_ARN, + "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"), + entry( + ResourceAttributes.AWS_LOG_GROUP_NAMES, Collections.singletonList("/ecs/metadata")), + entry( + ResourceAttributes.AWS_LOG_GROUP_ARNS, + Collections.singletonList( + "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata")), + entry( + ResourceAttributes.AWS_LOG_STREAM_NAMES, + Collections.singletonList("ecs/curl/8f03e41243824aea923aca126495f665")), + entry( + ResourceAttributes.AWS_LOG_STREAM_ARNS, + Collections.singletonList( + "arn:aws:logs:us-west-2:111122223333:log-group:/ecs/metadata:log-stream:ecs/curl/8f03e41243824aea923aca126495f665")), + entry( + ResourceAttributes.AWS_ECS_TASK_ARN, + "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"), + entry(ResourceAttributes.AWS_ECS_LAUNCHTYPE, "ec2"), + entry(ResourceAttributes.AWS_ECS_TASK_FAMILY, "curltest"), + entry(ResourceAttributes.AWS_ECS_TASK_REVISION, "26")); + } + + @Test + void testNotOnEcs() { + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V3, ""); + mockSysEnv.put(ECS_METADATA_KEY_V4, ""); + Attributes attributes = EcsResource.buildResource(mockSysEnv, mockHttpClient).getAttributes(); + assertThat(attributes).isEmpty(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(EcsResourceProvider.class::isInstance); + } + + String readResourceJson(String resourceName) throws IOException { + return Resources.toString(Resources.getResource(resourceName), StandardCharsets.UTF_8); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java new file mode 100644 index 00000000..509be8d9 --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/EksResourceTest.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static io.opentelemetry.contrib.aws.resource.EksResource.AUTH_CONFIGMAP_PATH; +import static io.opentelemetry.contrib.aws.resource.EksResource.CW_CONFIGMAP_PATH; +import static io.opentelemetry.contrib.aws.resource.EksResource.K8S_SVC_URL; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.io.File; +import java.io.IOException; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class EksResourceTest { + + @Mock private DockerHelper mockDockerHelper; + + @Mock private SimpleHttpClient httpClient; + + @Test + void testEks(@TempDir File tempFolder) throws IOException { + File mockK8sTokenFile = new File(tempFolder, "k8sToken"); + String token = "token123"; + Files.write(token.getBytes(Charsets.UTF_8), mockK8sTokenFile); + File mockK8sKeystoreFile = new File(tempFolder, "k8sCert"); + String truststore = "truststore123"; + Files.write(truststore.getBytes(Charsets.UTF_8), mockK8sKeystoreFile); + + when(httpClient.fetchString(any(), Mockito.eq(K8S_SVC_URL + AUTH_CONFIGMAP_PATH), any(), any())) + .thenReturn("not empty"); + when(httpClient.fetchString(any(), Mockito.eq(K8S_SVC_URL + CW_CONFIGMAP_PATH), any(), any())) + .thenReturn("{\"data\":{\"cluster.name\":\"my-cluster\"}}"); + when(mockDockerHelper.getContainerId()).thenReturn("0123456789A"); + + Resource eksResource = + EksResource.buildResource( + httpClient, + mockDockerHelper, + mockK8sTokenFile.getPath(), + mockK8sKeystoreFile.getPath()); + Attributes attributes = eksResource.getAttributes(); + + assertThat(eksResource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_eks"), + entry(ResourceAttributes.K8S_CLUSTER_NAME, "my-cluster"), + entry(ResourceAttributes.CONTAINER_ID, "0123456789A")); + } + + @Test + void testNotEks() { + Resource eksResource = EksResource.buildResource(httpClient, mockDockerHelper, "", ""); + Attributes attributes = eksResource.getAttributes(); + assertThat(attributes).isEmpty(); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(EksResourceProvider.class::isInstance); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/LambdaResourceTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/LambdaResourceTest.java new file mode 100644 index 00000000..e4a35a59 --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/LambdaResourceTest.java @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.entry; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.semconv.resource.attributes.ResourceAttributes; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +class LambdaResourceTest { + @Test + void shouldNotCreateResourceForNotLambda() { + Attributes attributes = LambdaResource.buildResource(emptyMap()).getAttributes(); + assertThat(attributes).isEmpty(); + } + + @Test + void shouldAddNonEmptyAttributes() { + Resource resource = + LambdaResource.buildResource(singletonMap("AWS_LAMBDA_FUNCTION_NAME", "my-function")); + Attributes attributes = resource.getAttributes(); + + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_lambda"), + entry(ResourceAttributes.FAAS_NAME, "my-function")); + } + + @Test + void shouldAddAllAttributes() { + Map envVars = new HashMap<>(); + envVars.put("AWS_REGION", "us-east-1"); + envVars.put("AWS_LAMBDA_FUNCTION_NAME", "my-function"); + envVars.put("AWS_LAMBDA_FUNCTION_VERSION", "1.2.3"); + + Resource resource = LambdaResource.buildResource(envVars); + Attributes attributes = resource.getAttributes(); + + assertThat(resource.getSchemaUrl()).isEqualTo(ResourceAttributes.SCHEMA_URL); + assertThat(attributes) + .containsOnly( + entry(ResourceAttributes.CLOUD_PROVIDER, "aws"), + entry(ResourceAttributes.CLOUD_PLATFORM, "aws_lambda"), + entry(ResourceAttributes.CLOUD_REGION, "us-east-1"), + entry(ResourceAttributes.FAAS_NAME, "my-function"), + entry(ResourceAttributes.FAAS_VERSION, "1.2.3")); + } + + @Test + void inServiceLoader() { + // No practical way to test the attributes themselves so at least check the service loader picks + // it up. + assertThat(ServiceLoader.load(ResourceProvider.class)) + .anyMatch(LambdaResourceProvider.class::isInstance); + } +} diff --git a/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClientTest.java b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClientTest.java new file mode 100644 index 00000000..633cd75e --- /dev/null +++ b/aws-resources/src/test/java/io/opentelemetry/contrib/aws/resource/SimpleHttpClientTest.java @@ -0,0 +1,113 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.aws.resource; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import com.linecorp.armeria.common.AggregatedHttpRequest; +import com.linecorp.armeria.common.HttpResponse; +import com.linecorp.armeria.server.ServerBuilder; +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import com.linecorp.armeria.testing.junit5.server.ServerExtension; +import com.linecorp.armeria.testing.junit5.server.mock.MockWebServerExtension; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.io.TempDir; + +class SimpleHttpClientTest { + + @RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension(); + + @Test + void testFetchString() { + server.enqueue(HttpResponse.of("expected result")); + + ImmutableMap requestPropertyMap = + ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + SimpleHttpClient httpClient = new SimpleHttpClient(); + String result = httpClient.fetchString("GET", urlStr, requestPropertyMap, null); + + assertThat(result).isEqualTo("expected result"); + + AggregatedHttpRequest request1 = server.takeRequest().request(); + assertThat(request1.path()).isEqualTo("/path"); + assertThat(request1.headers().get("key1")).isEqualTo("value1"); + assertThat(request1.headers().get("key2")).isEqualTo("value2"); + } + + @Test + void testFailedFetchString() { + ImmutableMap requestPropertyMap = + ImmutableMap.of("key1", "value1", "key2", "value2"); + String urlStr = String.format("http://localhost:%s%s", server.httpPort(), "/path"); + SimpleHttpClient httpClient = new SimpleHttpClient(); + String result = httpClient.fetchString("GET", urlStr, requestPropertyMap, null); + assertThat(result).isEmpty(); + } + + static class HttpsServerTest { + @RegisterExtension + @Order(1) + public static SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + @RegisterExtension + @Order(2) + public static ServerExtension server = + new ServerExtension() { + @Override + protected void configure(ServerBuilder sb) { + sb.tls(certificate.certificateFile(), certificate.privateKeyFile()); + + sb.service("/", (ctx, req) -> HttpResponse.of("Thanks for trusting me")); + } + }; + + @Test + void goodCert() { + SimpleHttpClient httpClient = new SimpleHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certificate.certificateFile().getAbsolutePath()); + assertThat(result).isEqualTo("Thanks for trusting me"); + } + + @Test + void missingCert() { + SimpleHttpClient httpClient = new SimpleHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + "/foo/bar/bad"); + assertThat(result).isEmpty(); + } + + @Test + void badCert(@TempDir Path tempDir) throws Exception { + Path certFile = tempDir.resolve("test.crt"); + Files.write(certFile, "bad cert".getBytes(StandardCharsets.UTF_8)); + SimpleHttpClient httpClient = new SimpleHttpClient(); + String result = + httpClient.fetchString( + "GET", + "https://localhost:" + server.httpsPort() + "/", + Collections.emptyMap(), + certFile.toString()); + assertThat(result).isEmpty(); + } + } +} diff --git a/aws-resources/src/test/resources/ecs-container-metadata-v3.json b/aws-resources/src/test/resources/ecs-container-metadata-v3.json new file mode 100644 index 00000000..2e89ffe5 --- /dev/null +++ b/aws-resources/src/test/resources/ecs-container-metadata-v3.json @@ -0,0 +1,31 @@ +{ + "DockerId": "43481a6ce4842eec8fe72fc28500c6b52edcc0917f105b83379f88cac1ff3946", + "Name": "nginx-curl", + "DockerName": "ecs-nginx-5-nginx-curl-ccccb9f49db0dfe0d901", + "Image": "nrdlngr/nginx-curl", + "ImageID": "sha256:2e00ae64383cfc865ba0a2ba37f61b50a120d2d9378559dcd458dc0de47bc165", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "nginx-curl", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3", + "com.amazonaws.ecs.task-definition-family": "nginx", + "com.amazonaws.ecs.task-definition-version": "5" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 512, + "Memory": 512 + }, + "CreatedAt": "2018-02-01T20:55:10.554941919Z", + "StartedAt": "2018-02-01T20:55:11.064236631Z", + "Type": "NORMAL", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.106" + ] + } + ] +} \ No newline at end of file diff --git a/aws-resources/src/test/resources/ecs-container-metadata-v4.json b/aws-resources/src/test/resources/ecs-container-metadata-v4.json new file mode 100644 index 00000000..0001b509 --- /dev/null +++ b/aws-resources/src/test/resources/ecs-container-metadata-v4.json @@ -0,0 +1,44 @@ +{ + "DockerId": "ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66", + "Name": "curl", + "DockerName": "ecs-curltest-26-curl-cca48e8dcadd97805600", + "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", + "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "curl", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/8f03e41243824aea923aca126495f665", + "com.amazonaws.ecs.task-definition-family": "curltest", + "com.amazonaws.ecs.task-definition-version": "26" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 10, + "Memory": 128 + }, + "CreatedAt": "2020-10-02T00:15:07.620912337Z", + "StartedAt": "2020-10-02T00:15:08.062559351Z", + "Type": "NORMAL", + "LogDriver": "awslogs", + "LogOptions": { + "awslogs-create-group": "true", + "awslogs-group": "/ecs/metadata", + "awslogs-region": "us-west-2", + "awslogs-stream": "ecs/curl/8f03e41243824aea923aca126495f665" + }, + "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.100" + ], + "AttachmentIndex": 0, + "MACAddress": "0e:9e:32:c7:48:85", + "IPv4SubnetCIDRBlock": "10.0.2.0/24", + "PrivateDNSName": "ip-10-0-2-100.us-west-2.compute.internal", + "SubnetGatewayIpv4Address": "10.0.2.1/24" + } + ] +} \ No newline at end of file diff --git a/aws-resources/src/test/resources/ecs-task-metadata-v3.json b/aws-resources/src/test/resources/ecs-task-metadata-v3.json new file mode 100644 index 00000000..69b72407 --- /dev/null +++ b/aws-resources/src/test/resources/ecs-task-metadata-v3.json @@ -0,0 +1,75 @@ +{ + "Cluster": "default", + "TaskARN": "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3", + "Family": "nginx", + "Revision": "5", + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Containers": [ + { + "DockerId": "731a0d6a3b4210e2448339bc7015aaa79bfe4fa256384f4102db86ef94cbbc4c", + "Name": "~internal~ecs~pause", + "DockerName": "ecs-nginx-5-internalecspause-acc699c0cbf2d6d11700", + "Image": "amazon/amazon-ecs-pause:0.1.0", + "ImageID": "", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "~internal~ecs~pause", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3", + "com.amazonaws.ecs.task-definition-family": "nginx", + "com.amazonaws.ecs.task-definition-version": "5" + }, + "DesiredStatus": "RESOURCES_PROVISIONED", + "KnownStatus": "RESOURCES_PROVISIONED", + "Limits": { + "CPU": 0, + "Memory": 0 + }, + "CreatedAt": "2018-02-01T20:55:08.366329616Z", + "StartedAt": "2018-02-01T20:55:09.058354915Z", + "Type": "CNI_PAUSE", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.106" + ] + } + ] + }, + { + "DockerId": "43481a6ce4842eec8fe72fc28500c6b52edcc0917f105b83379f88cac1ff3946", + "Name": "nginx-curl", + "DockerName": "ecs-nginx-5-nginx-curl-ccccb9f49db0dfe0d901", + "Image": "nrdlngr/nginx-curl", + "ImageID": "sha256:2e00ae64383cfc865ba0a2ba37f61b50a120d2d9378559dcd458dc0de47bc165", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "nginx-curl", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-2:012345678910:task/9781c248-0edd-4cdb-9a93-f63cb662a5d3", + "com.amazonaws.ecs.task-definition-family": "nginx", + "com.amazonaws.ecs.task-definition-version": "5" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 512, + "Memory": 512 + }, + "CreatedAt": "2018-02-01T20:55:10.554941919Z", + "StartedAt": "2018-02-01T20:55:11.064236631Z", + "Type": "NORMAL", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.106" + ] + } + ] + } + ], + "PullStartedAt": "2018-02-01T20:55:09.372495529Z", + "PullStoppedAt": "2018-02-01T20:55:10.552018345Z", + "AvailabilityZone": "us-east-2b" +} \ No newline at end of file diff --git a/aws-resources/src/test/resources/ecs-task-metadata-v4.json b/aws-resources/src/test/resources/ecs-task-metadata-v4.json new file mode 100644 index 00000000..c8043a74 --- /dev/null +++ b/aws-resources/src/test/resources/ecs-task-metadata-v4.json @@ -0,0 +1,94 @@ +{ + "Cluster": "default", + "TaskARN": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", + "Family": "curltest", + "Revision": "26", + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "PullStartedAt": "2020-10-02T00:43:06.202617438Z", + "PullStoppedAt": "2020-10-02T00:43:06.31288465Z", + "AvailabilityZone": "us-west-2d", + "LaunchType": "EC2", + "Containers": [ + { + "DockerId": "598cba581fe3f939459eaba1e071d5c93bb2c49b7d1ba7db6bb19deeb70d8e38", + "Name": "~internal~ecs~pause", + "DockerName": "ecs-curltest-26-internalecspause-e292d586b6f9dade4a00", + "Image": "amazon/amazon-ecs-pause:0.1.0", + "ImageID": "", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "~internal~ecs~pause", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", + "com.amazonaws.ecs.task-definition-family": "curltest", + "com.amazonaws.ecs.task-definition-version": "26" + }, + "DesiredStatus": "RESOURCES_PROVISIONED", + "KnownStatus": "RESOURCES_PROVISIONED", + "Limits": { + "CPU": 0, + "Memory": 0 + }, + "CreatedAt": "2020-10-02T00:43:05.602352471Z", + "StartedAt": "2020-10-02T00:43:06.076707576Z", + "Type": "CNI_PAUSE", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.61" + ], + "AttachmentIndex": 0, + "MACAddress": "0e:10:e2:01:bd:91", + "IPv4SubnetCIDRBlock": "10.0.2.0/24", + "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal", + "SubnetGatewayIpv4Address": "10.0.2.1/24" + } + ] + }, + { + "DockerId": "ee08638adaaf009d78c248913f629e38299471d45fe7dc944d1039077e3424ca", + "Name": "curl", + "DockerName": "ecs-curltest-26-curl-a0e7dba5aca6d8cb2e00", + "Image": "111122223333.dkr.ecr.us-west-2.amazonaws.com/curltest:latest", + "ImageID": "sha256:d691691e9652791a60114e67b365688d20d19940dde7c4736ea30e660d8d3553", + "Labels": { + "com.amazonaws.ecs.cluster": "default", + "com.amazonaws.ecs.container-name": "curl", + "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", + "com.amazonaws.ecs.task-definition-family": "curltest", + "com.amazonaws.ecs.task-definition-version": "26" + }, + "DesiredStatus": "RUNNING", + "KnownStatus": "RUNNING", + "Limits": { + "CPU": 10, + "Memory": 128 + }, + "CreatedAt": "2020-10-02T00:43:06.326590752Z", + "StartedAt": "2020-10-02T00:43:06.767535449Z", + "Type": "NORMAL", + "LogDriver": "awslogs", + "LogOptions": { + "awslogs-create-group": "true", + "awslogs-group": "/ecs/metadata", + "awslogs-region": "us-west-2", + "awslogs-stream": "ecs/curl/158d1c8083dd49d6b527399fd6414f5c" + }, + "ContainerARN": "arn:aws:ecs:us-west-2:111122223333:container/abb51bdd-11b4-467f-8f6c-adcfe1fe059d", + "Networks": [ + { + "NetworkMode": "awsvpc", + "IPv4Addresses": [ + "10.0.2.61" + ], + "AttachmentIndex": 0, + "MACAddress": "0e:10:e2:01:bd:91", + "IPv4SubnetCIDRBlock": "10.0.2.0/24", + "PrivateDNSName": "ip-10-0-2-61.us-west-2.compute.internal", + "SubnetGatewayIpv4Address": "10.0.2.1/24" + } + ] + } + ] +} \ No newline at end of file diff --git a/aws-resources/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/aws-resources/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..1f0955d4 --- /dev/null +++ b/aws-resources/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 41a4da52..edd5750d 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -50,7 +50,7 @@ val DEPENDENCY_SETS = listOf( ), DependencySet( "org.mockito", - "4.3.1", + "4.7.0", listOf("mockito-core", "mockito-junit-jupiter") ), DependencySet( @@ -68,6 +68,7 @@ val DEPENDENCIES = listOf( "com.uber.nullaway:nullaway:0.9.5", "org.assertj:assertj-core:3.22.0", "org.awaitility:awaitility:4.1.1", + "org.bouncycastle:bcpkix-jdk15on:1.70", "org.junit-pioneer:junit-pioneer:1.7.0", "org.skyscreamer:jsonassert:1.5.0" ) diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e3f1561..254821ee 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,6 +38,7 @@ gradleEnterprise { rootProject.name = "opentelemetry-java-contrib" include(":all") +include(":aws-resources") include(":aws-xray") include(":consistent-sampling") include(":dependencyManagement")