Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement aws.ecs.* resource attributes #4670

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand All @@ -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();

/**
Expand Down Expand Up @@ -61,6 +68,85 @@ static Resource buildResource(Map<String, String> 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);
}

Expand All @@ -69,5 +155,32 @@ private static boolean isOnEcs(Map<String, String> 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() {}
}
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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<String, String> mockSysEnv = new HashMap<>();
mockSysEnv.put(ECS_METADATA_KEY_V3, "ecs_metadata_v3_uri");
Expand All @@ -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<String, String> 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<String, String> mockSysEnv = new HashMap<>();
Expand All @@ -60,7 +243,7 @@ void testNotOnEcs() {
void testContainerIdMissing() throws UnknownHostException {
when(mockDockerHelper.getContainerId()).thenReturn("");
Map<String, String> 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(
Expand Down