diff --git a/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java b/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java index 913439eda34..c03841e4746 100644 --- a/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java +++ b/sdk-extensions/aws/src/main/java/io/opentelemetry/sdk/extension/aws/resource/EcsResource.java @@ -5,12 +5,17 @@ package io.opentelemetry.sdk.extension.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.InetAddress; import java.net.UnknownHostException; +import java.util.Collections; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -25,6 +30,8 @@ public final class EcsResource { 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 JsonFactory JSON_FACTORY = new JsonFactory(); + private static final Resource INSTANCE = buildResource(); /** @@ -61,6 +68,85 @@ static Resource buildResource(Map sysEnv, DockerHelper dockerHel attrBuilders.put(ResourceAttributes.CONTAINER_ID, containerId); } + String metadataUriV4 = sysEnv.getOrDefault(ECS_METADATA_KEY_V4, ""); + if (!metadataUriV4.isEmpty()) { + try { + attrBuilders.put( + ResourceAttributes.AWS_ECS_CONTAINER_ARN, + getContainerArn(fetchMetadata(metadataUriV4))); + } catch (Exception e) { + logger.log( + Level.WARNING, "Could not get the container ARN from the Metadata V4 endpoint.", e); + } + + try { + String taskMetadata = fetchMetadata(metadataUriV4 + "/task"); + + try (JsonParser parser = JSON_FACTORY.createParser(taskMetadata)) { + parser.nextToken(); + + if (!parser.isExpectedStartObjectToken()) { + throw new IOException( + "Invalid JSON returned by the Metadata v4 '/task' endpoint:" + taskMetadata); + } + + String taskArn = null; + String cluster = null; + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + switch (parser.getCurrentName()) { + case "Cluster": + /* + * This is not guaranteed to be the Cluster ARN, and we may also need the + * Task ARN to recreate the cluster ARN. + */ + cluster = value; + break; + case "Family": + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_FAMILY, value); + break; + case "LaunchType": + attrBuilders.put(ResourceAttributes.AWS_ECS_LAUNCHTYPE, value); + break; + case "Revision": + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_REVISION, value); + break; + case "TaskARN": + taskArn = value; + attrBuilders.put(ResourceAttributes.AWS_ECS_TASK_ARN, value); + break; + default: + parser.skipChildren(); + } + } + + if (taskArn == null) { + throw new IllegalStateException( + "The 'TaskARN' field was not provided by the Metadata v4 '/task' endpoint:" + + taskMetadata); + } + + if (cluster == null) { + throw new IllegalStateException( + "The 'Cluster' field was not provided by the Metadata v4 '/task' endpoint:" + + taskMetadata); + } else if (cluster.startsWith("arn:")) { + attrBuilders.put(ResourceAttributes.AWS_ECS_CLUSTER_ARN, cluster); + } else { + String baseArn = taskArn.substring(0, taskArn.lastIndexOf(":")); + attrBuilders.put( + ResourceAttributes.AWS_ECS_CLUSTER_ARN, baseArn + ":cluster/" + cluster); + } + } + } catch (Exception e) { + logger.log( + Level.WARNING, + "Could not extract resource attributes from the Metadata V4 '/task' endpoint.", + e); + } + } + return Resource.create(attrBuilders.build(), ResourceAttributes.SCHEMA_URL); } @@ -69,5 +155,32 @@ private static boolean isOnEcs(Map sysEnv) { || !sysEnv.getOrDefault(ECS_METADATA_KEY_V4, "").isEmpty(); } + private static String getContainerArn(String containerMetadataJson) throws IOException { + JsonParser parser = JSON_FACTORY.createParser(containerMetadataJson); + + parser.nextToken(); + if (!parser.isExpectedStartObjectToken()) { + throw new IOException( + "Invalid JSON returned by the Metadata v4 endpoint:" + containerMetadataJson); + } + + while (parser.nextToken() != JsonToken.END_OBJECT) { + String value = parser.nextTextValue(); + switch (parser.getCurrentName()) { + case "ContainerARN": + return value; + default: + parser.skipChildren(); + } + } + + throw new IllegalStateException( + "The JSON returned by the ECS Metadata V4 endpoint does not contain a 'ContainerARN' field."); + } + + private static String fetchMetadata(String url) { + return new SimpleHttpClient().fetchString("GET", url.toString(), Collections.emptyMap(), null); + } + private EcsResource() {} } diff --git a/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java b/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java index 7ac1a81122c..2d8e76d6353 100644 --- a/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java +++ b/sdk-extensions/aws/src/test/java/io/opentelemetry/sdk/extension/aws/resource/EcsResourceTest.java @@ -9,6 +9,9 @@ import static org.assertj.core.api.Assertions.entry; import static org.mockito.Mockito.when; +import com.linecorp.armeria.common.HttpResponse; +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; @@ -20,6 +23,7 @@ import java.util.ServiceLoader; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -28,10 +32,154 @@ 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"; + private static final String METADATA_V4_RESPONSE = + "{" + + " \"DockerId\": \"ea32192c8553fbff06c9340478a2ff089b2bb5646fb718b4ee206641c9086d66\"," + + " \"Name\": \"curl\"," + + " \"DockerName\": \"ecs-curltest-24-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\": \"24\"" + + " }," + + " \"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\"" + + " }" + + " ]" + + "}"; + + private static final String METADATA_V4_TASK_RESPONSE = + "{" + + " \"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\"" + + " }" + + " ]" + + " }" + + " ]" + + "}"; + + @RegisterExtension public static MockWebServerExtension server = new MockWebServerExtension(); + @Mock private DockerHelper mockDockerHelper; @Test - void testCreateAttributes() throws UnknownHostException { + void testCreateAttributesMetadataV3() throws UnknownHostException { when(mockDockerHelper.getContainerId()).thenReturn("0123456789A"); Map mockSysEnv = new HashMap<>(); mockSysEnv.put(ECS_METADATA_KEY_V3, "ecs_metadata_v3_uri"); @@ -47,6 +195,41 @@ void testCreateAttributes() throws UnknownHostException { entry(ResourceAttributes.CONTAINER_ID, "0123456789A")); } + @Test + void testCreateAttributesMetadataV4() throws UnknownHostException { + when(mockDockerHelper.getContainerId()).thenReturn("0123456789A"); + + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, METADATA_V4_RESPONSE)); + server.enqueue(HttpResponse.of(MediaType.JSON_UTF_8, METADATA_V4_TASK_RESPONSE)); + + Map mockSysEnv = new HashMap<>(); + mockSysEnv.put(ECS_METADATA_KEY_V3, "http://ecs_metadata_v3_uri"); + mockSysEnv.put(ECS_METADATA_KEY_V4, "http://localhost:" + server.httpPort()); + + Resource resource = EcsResource.buildResource(mockSysEnv, mockDockerHelper); + 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, InetAddress.getLocalHost().getHostName()), + entry(ResourceAttributes.CONTAINER_ID, "0123456789A"), + entry( + ResourceAttributes.AWS_ECS_CLUSTER_ARN, + "arn:aws:ecs:us-west-2:111122223333:cluster/default"), + entry( + ResourceAttributes.AWS_ECS_CONTAINER_ARN, + "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9"), + entry(ResourceAttributes.AWS_ECS_LAUNCHTYPE, "EC2"), + entry( + ResourceAttributes.AWS_ECS_TASK_ARN, + "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c"), + entry(ResourceAttributes.AWS_ECS_TASK_FAMILY, "curltest"), + entry(ResourceAttributes.AWS_ECS_TASK_REVISION, "26")); + } + @Test void testNotOnEcs() { Map mockSysEnv = new HashMap<>(); @@ -60,7 +243,7 @@ void testNotOnEcs() { void testContainerIdMissing() throws UnknownHostException { when(mockDockerHelper.getContainerId()).thenReturn(""); Map mockSysEnv = new HashMap<>(); - mockSysEnv.put(ECS_METADATA_KEY_V4, "ecs_metadata_v4_uri"); + mockSysEnv.put(ECS_METADATA_KEY_V3, "ecs_metadata_v3_uri"); Attributes attributes = EcsResource.buildResource(mockSysEnv, mockDockerHelper).getAttributes(); assertThat(attributes) .containsOnly(