diff --git a/CHANGELOG.md b/CHANGELOG.md index 69e5f9c268..723fc6402d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1206](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1206)) - Add psycopg2 native tags to sqlcommenter ([#1203](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1203)) +- Implement [`aws.ecs.*` resource attributes](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/semantic_conventions/cloud_provider/aws/ecs.md) in the `AwsEcsResourceDetector` detector + ([#1212](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1212)) ### Added - `opentelemetry-instrumentation-redis` add support to instrument RedisCluster clients diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/ecs.py b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/ecs.py index 3b0d6d79a1..20e26f2623 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/ecs.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/src/opentelemetry/sdk/extension/aws/resource/ecs.py @@ -12,10 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json import logging import os import socket +from urllib.request import Request, urlopen + from opentelemetry.sdk.resources import Resource, ResourceDetector from opentelemetry.semconv.resource import ( CloudPlatformValues, @@ -58,7 +61,7 @@ def detect(self) -> "Resource": "Failed to get container ID on ECS: %s.", exception ) - return Resource( + base_resource = Resource( { ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value, ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ECS.value, @@ -66,6 +69,49 @@ def detect(self) -> "Resource": ResourceAttributes.CONTAINER_ID: container_id, } ) + + metadata_v4_endpoint = os.environ.get( + "ECS_CONTAINER_METADATA_URI_V4" + ) + + if not metadata_v4_endpoint: + return base_resource + + # Returns https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html#task-metadata-endpoint-v4-response + metadata_container = json.loads(_http_get(metadata_v4_endpoint)) + metadata_task = json.loads( + _http_get(f"{metadata_v4_endpoint}/task") + ) + + task_arn = metadata_task["TaskARN"] + base_arn = task_arn[0 : task_arn.rindex(":")] # noqa + cluster: str = metadata_task["Cluster"] + cluster_arn = ( + cluster + if cluster.startswith("arn:") + else f"{base_arn}:cluster/{cluster}" + ) + + return base_resource.merge( + Resource( + { + ResourceAttributes.AWS_ECS_CONTAINER_ARN: metadata_container[ + "ContainerARN" + ], + ResourceAttributes.AWS_ECS_CLUSTER_ARN: cluster_arn, + ResourceAttributes.AWS_ECS_LAUNCHTYPE: metadata_task[ + "LaunchType" + ], + ResourceAttributes.AWS_ECS_TASK_ARN: task_arn, + ResourceAttributes.AWS_ECS_TASK_FAMILY: metadata_task[ + "Family" + ], + ResourceAttributes.AWS_ECS_TASK_REVISION: metadata_task[ + "Revision" + ], + } + ) + ) # pylint: disable=broad-except except Exception as exception: if self.raise_on_error: @@ -73,3 +119,11 @@ def detect(self) -> "Resource": logger.warning("%s failed: %s", self.__class__.__name__, exception) return Resource.get_empty() + + +def _http_get(url): + with urlopen( + Request(url, method="GET"), + timeout=5, + ) as response: + return response.read().decode("utf-8") diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-container.json b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-container.json new file mode 100644 index 0000000000..b43c2b0d7d --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-container.json @@ -0,0 +1,44 @@ +{ + "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" + } + ] +} diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-task.json b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-task.json new file mode 100644 index 0000000000..101efe0214 --- /dev/null +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/metadatav4-response-task.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" + } + ] + } + ] +} diff --git a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py index 04931e1b76..b73d0c2ccf 100644 --- a/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py +++ b/sdk-extension/opentelemetry-sdk-extension-aws/tests/resource/test_ecs.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os import unittest from collections import OrderedDict from unittest.mock import mock_open, patch @@ -31,6 +32,27 @@ } +def _read_file(filename: str) -> str: + with open(os.path.join(os.path.dirname(__file__), filename)) as f: + return f.read() + + +MetadataV4Uri = "mock-uri-4" + + +MetadataV4ContainerResponse = _read_file("metadatav4-response-container.json") + + +MetadataV4TaskResponse = _read_file("metadatav4-response-task.json") + + +def _http_get_function(url: str, *args, **kwargs) -> str: + if url == MetadataV4Uri: + return MetadataV4ContainerResponse + if url == f"{MetadataV4Uri}/task": + return MetadataV4TaskResponse + + class AwsEcsResourceDetectorTest(unittest.TestCase): @patch.dict( "os.environ", @@ -60,8 +82,55 @@ class AwsEcsResourceDetectorTest(unittest.TestCase): 1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked """, ) - def test_simple_create(self, mock_open_function, mock_socket_gethostname): + def test_simple_create_metadata_v3(self, mock_open_function, mock_socket_gethostname): actual = AwsEcsResourceDetector().detect() self.assertDictEqual( actual.attributes.copy(), OrderedDict(MockEcsResourceAttributes) ) + + @patch.dict( + "os.environ", + {"ECS_CONTAINER_METADATA_URI_V4": MetadataV4Uri}, + clear=True, + ) + @patch( + "socket.gethostname", + return_value=f"{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_NAME]}", + ) + @patch( + "builtins.open", + new_callable=mock_open, + read_data=f"""14:name=systemd:/docker/{MockEcsResourceAttributes[ResourceAttributes.CONTAINER_ID]} +13:rdma:/ +12:pids:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +11:hugetlb:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +10:net_prio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +9:perf_event:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +8:net_cls:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +7:freezer:/docker/ +6:devices:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +5:memory:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +4:blkio:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +3:cpuacct:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +2:cpu:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +1:cpuset:/docker/bogusContainerIdThatShouldNotBeOneSetBecauseTheFirstOneWasPicked +""", + ) + @patch( + "opentelemetry.sdk.extension.aws.resource.ecs._http_get", + ) + def test_simple_create_metadata_v4(self, mock_http_get_function, mock_open_function, mock_socket_gethostname): + mock_http_get_function.side_effect = _http_get_function + actual = AwsEcsResourceDetector().detect() + self.assertDictEqual( + actual.attributes.copy(), + OrderedDict({ + **MockEcsResourceAttributes, + ResourceAttributes.AWS_ECS_CONTAINER_ARN: "arn:aws:ecs:us-west-2:111122223333:container/0206b271-b33f-47ab-86c6-a0ba208a70a9", + ResourceAttributes.AWS_ECS_CLUSTER_ARN: "arn:aws:ecs:us-west-2:111122223333:cluster/default", + ResourceAttributes.AWS_ECS_LAUNCHTYPE: "EC2", + ResourceAttributes.AWS_ECS_TASK_ARN: "arn:aws:ecs:us-west-2:111122223333:task/default/158d1c8083dd49d6b527399fd6414f5c", + ResourceAttributes.AWS_ECS_TASK_FAMILY: "curltest", + ResourceAttributes.AWS_ECS_TASK_REVISION: "26", + }) + )