Skip to content

Commit

Permalink
Add AWS resource detectors to extension package (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanielRN committed Jul 23, 2021
1 parent 5374aee commit 1157eb2
Show file tree
Hide file tree
Showing 13 changed files with 851 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.4.0-0.23b0...HEAD)
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))

## [1.4.0-0.23b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.4.0-0.23b0) - 2021-07-21

Expand Down
32 changes: 32 additions & 0 deletions sdk-extension/opentelemetry-sdk-extension-aws/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,40 @@ Or by setting this propagator in your instrumented application:
set_global_textmap(AwsXRayFormat())
Usage (AWS Resource Detectors)
------------------------------

Use the provided `Resource Detectors` to automatically populate attributes under the `resource`
namespace of each generated span.

For example, if tracing with OpenTelemetry on an AWS EC2 instance, you can automatically
populate `resource` attributes by creating a `TraceProvider` using the `AwsEc2ResourceDetector`:

.. code-block:: python
import opentelemetry.trace as trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.extension.aws.resource.ec2 import (
AwsEc2ResourceDetector,
)
from opentelemetry.sdk.resources import get_aggregated_resources
trace.set_tracer_provider(
TracerProvider(
resource=get_aggregated_resources(
[
AwsEc2ResourceDetector(),
]
),
)
)
Refer to each detectors' docstring to determine any possible requirements for that
detector.

References
----------

* `OpenTelemetry Project <https://opentelemetry.io/>`_
* `AWS X-Ray Trace IDs Format <https://docs.aws.amazon.com/xray/latest/devguide/xray-api-sendingdata.html#xray-api-traceids>`_
* `OpenTelemetry Specification for Resource Attributes <https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/resource/semantic_conventions>`_
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from opentelemetry.sdk.extension.aws.resource._lambda import (
AwsLambdaResourceDetector,
)
from opentelemetry.sdk.extension.aws.resource.beanstalk import (
AwsBeanstalkResourceDetector,
)
from opentelemetry.sdk.extension.aws.resource.ec2 import AwsEc2ResourceDetector
from opentelemetry.sdk.extension.aws.resource.ecs import AwsEcsResourceDetector
from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector

__all__ = [
"AwsBeanstalkResourceDetector",
"AwsEc2ResourceDetector",
"AwsEcsResourceDetector",
"AwsEksResourceDetector",
"AwsLambdaResourceDetector",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from os import environ

from opentelemetry.sdk.resources import Resource, ResourceDetector
from opentelemetry.semconv.resource import (
CloudPlatformValues,
CloudProviderValues,
ResourceAttributes,
)

logger = logging.getLogger(__name__)


class AwsLambdaResourceDetector(ResourceDetector):
"""Detects attribute values only available when the app is running on AWS
Lambda and returns them in a Resource.
Uses Lambda defined runtime enivronment variables. See more: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html#configuration-envvars-runtime
"""

def detect(self) -> "Resource":
try:
return Resource(
{
ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value,
ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_LAMBDA.value,
ResourceAttributes.CLOUD_REGION: environ["AWS_REGION"],
ResourceAttributes.FAAS_NAME: environ[
"AWS_LAMBDA_FUNCTION_NAME"
],
ResourceAttributes.FAAS_VERSION: environ[
"AWS_LAMBDA_FUNCTION_VERSION"
],
ResourceAttributes.FAAS_INSTANCE: environ[
"AWS_LAMBDA_LOG_STREAM_NAME"
],
ResourceAttributes.FAAS_MAX_MEMORY: int(
environ["AWS_LAMBDA_FUNCTION_MEMORY_SIZE"]
),
}
)
# pylint: disable=broad-except
except Exception as exception:
if self.raise_on_error:
raise exception

logger.warning("%s failed: %s", self.__class__.__name__, exception)
return Resource.get_empty()
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
import os

from opentelemetry.sdk.resources import Resource, ResourceDetector
from opentelemetry.semconv.resource import (
CloudPlatformValues,
CloudProviderValues,
ResourceAttributes,
)

logger = logging.getLogger(__name__)


class AwsBeanstalkResourceDetector(ResourceDetector):
"""Detects attribute values only available when the app is running on AWS
Elastic Beanstalk and returns them in a Resource.
NOTE: Requires enabling X-Ray on Beanstalk Environment. See more here: https://docs.aws.amazon.com/xray/latest/devguide/xray-services-beanstalk.html
"""

def detect(self) -> "Resource":
if os.name == "nt":
conf_file_path = (
"C:\\Program Files\\Amazon\\XRay\\environment.conf"
)
else:
conf_file_path = "/var/elasticbeanstalk/xray/environment.conf"

try:
with open(conf_file_path) as conf_file:
parsed_data = json.load(conf_file)

return Resource(
{
ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value,
ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value,
ResourceAttributes.SERVICE_NAME: CloudPlatformValues.AWS_ELASTIC_BEANSTALK.value,
ResourceAttributes.SERVICE_INSTANCE_ID: parsed_data[
"deployment_id"
],
ResourceAttributes.SERVICE_NAMESPACE: parsed_data[
"environment_name"
],
ResourceAttributes.SERVICE_VERSION: parsed_data[
"version_label"
],
}
)
# pylint: disable=broad-except
except Exception as exception:
if self.raise_on_error:
raise exception

logger.warning("%s failed: %s", self.__class__.__name__, exception)
return Resource.get_empty()
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import logging
from urllib.request import Request, urlopen

from opentelemetry.sdk.resources import Resource, ResourceDetector
from opentelemetry.semconv.resource import (
CloudPlatformValues,
CloudProviderValues,
ResourceAttributes,
)

logger = logging.getLogger(__name__)

_AWS_METADATA_TOKEN_HEADER = "X-aws-ec2-metadata-token"
_GET_METHOD = "GET"


def _aws_http_request(method, path, headers):
with urlopen(
Request(
"http://169.254.169.254" + path, headers=headers, method=method
),
timeout=1000,
) as response:
return response.read().decode("utf-8")


def _get_token():
return _aws_http_request(
"PUT",
"/latest/api/token",
{"X-aws-ec2-metadata-token-ttl-seconds": "60"},
)


def _get_identity(token):
return _aws_http_request(
_GET_METHOD,
"/latest/dynamic/instance-identity/document",
{_AWS_METADATA_TOKEN_HEADER: token},
)


def _get_host(token):
return _aws_http_request(
_GET_METHOD,
"/latest/meta-data/hostname",
{_AWS_METADATA_TOKEN_HEADER: token},
)


class AwsEc2ResourceDetector(ResourceDetector):
"""Detects attribute values only available when the app is running on AWS
Elastic Compute Cloud (EC2) and returns them in a Resource.
Uses a special URI to get instance meta-data. See more: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
"""

def detect(self) -> "Resource":
try:
token = _get_token()
identity_dict = json.loads(_get_identity(token))
hostname = _get_host(token)

return Resource(
{
ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value,
ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_EC2.value,
ResourceAttributes.CLOUD_ACCOUNT_ID: identity_dict[
"accountId"
],
ResourceAttributes.CLOUD_REGION: identity_dict["region"],
ResourceAttributes.CLOUD_AVAILABILITY_ZONE: identity_dict[
"availabilityZone"
],
ResourceAttributes.HOST_ID: identity_dict["instanceId"],
ResourceAttributes.HOST_TYPE: identity_dict[
"instanceType"
],
ResourceAttributes.HOST_NAME: hostname,
}
)
# pylint: disable=broad-except
except Exception as exception:
if self.raise_on_error:
raise exception

logger.warning("%s failed: %s", self.__class__.__name__, exception)
return Resource.get_empty()
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
import socket

from opentelemetry.sdk.resources import Resource, ResourceDetector
from opentelemetry.semconv.resource import (
CloudPlatformValues,
CloudProviderValues,
ResourceAttributes,
)

logger = logging.getLogger(__name__)

_CONTAINER_ID_LENGTH = 64


class AwsEcsResourceDetector(ResourceDetector):
"""Detects attribute values only available when the app is running on AWS
Elastic Container Service (ECS) and returns them in a Resource.
"""

def detect(self) -> "Resource":
try:
if not os.environ.get(
"ECS_CONTAINER_METADATA_URI"
) and not os.environ.get("ECS_CONTAINER_METADATA_URI_V4"):
raise RuntimeError(
"Missing ECS_CONTAINER_METADATA_URI therefore process is not on ECS."
)

container_id = ""
try:
with open(
"/proc/self/cgroup", encoding="utf8"
) as container_info_file:
for raw_line in container_info_file.readlines():
line = raw_line.strip()
if len(line) > _CONTAINER_ID_LENGTH:
container_id = line[-_CONTAINER_ID_LENGTH:]
except FileNotFoundError as exception:
logger.warning(
"Failed to get container ID on ECS: %s.", exception
)

return Resource(
{
ResourceAttributes.CLOUD_PROVIDER: CloudProviderValues.AWS.value,
ResourceAttributes.CLOUD_PLATFORM: CloudPlatformValues.AWS_ECS.value,
ResourceAttributes.CONTAINER_NAME: socket.gethostname(),
ResourceAttributes.CONTAINER_ID: container_id,
}
)
# pylint: disable=broad-except
except Exception as exception:
if self.raise_on_error:
raise exception

logger.warning("%s failed: %s", self.__class__.__name__, exception)
return Resource.get_empty()
Loading

0 comments on commit 1157eb2

Please sign in to comment.