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

Add AWS resource detectors to extension package #586

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
50a280c
Add raw implementation of all aws detector UNTESTED
NathanielRN Jul 10, 2021
14f450a
Update semantic conventions variables
NathanielRN Jul 12, 2021
0abe18b
Requests and attribute fixes
NathanielRN Jul 13, 2021
7b8cf1b
Move path of detectors to shorter path
NathanielRN Jul 13, 2021
2f919b3
Fix lambda directory name
NathanielRN Jul 13, 2021
36dd476
Verified lambda and beanstalk detectors
NathanielRN Jul 14, 2021
7bee607
Some fixes to ecs and eks detectors
NathanielRN Jul 14, 2021
2b04578
Small updates to EKS resource detector
NathanielRN Jul 14, 2021
a72190a
Add unit tests for all resource detectors
NathanielRN Jul 15, 2021
f11c96f
Add readme and documentation updates
NathanielRN Jul 15, 2021
cf539d2
Adds useful links to explain resource detection
NathanielRN Jul 15, 2021
6a1caaa
Update changelog
NathanielRN Jul 15, 2021
7a73524
Fix linter issues
NathanielRN Jul 15, 2021
0583ed4
Use correct black version to lint
NathanielRN Jul 15, 2021
e7f39ae
Use correct isort version to lint test files
NathanielRN Jul 15, 2021
49790cf
Address flake lint problems
NathanielRN Jul 15, 2021
5533fa1
Better naming for file stream objects for lint
NathanielRN Jul 15, 2021
5e2e67a
Remove my embrassing debug statements
NathanielRN Jul 15, 2021
d9762e6
Use raise_on_error to make Resource detect required
NathanielRN Jul 15, 2021
549b9e2
Make all debug log statements warnings instead
NathanielRN Jul 15, 2021
25a904a
Run linter on recent changes
NathanielRN Jul 15, 2021
3ada952
Address several lint issues with Exception too broad
NathanielRN Jul 20, 2021
4756278
Update test to match previous lint updates
NathanielRN Jul 20, 2021
aeb838f
Raise eks excep if k8 token fetch failed
NathanielRN Jul 20, 2021
9bd36f8
README subheading needs to be longer
NathanielRN Jul 20, 2021
cf0efc4
Small updates to address PR comments
NathanielRN Jul 21, 2021
e63224e
Add root prefix to ecs container file read
NathanielRN Jul 23, 2021
24e8326
Linter limits lines on exception message
NathanielRN Jul 23, 2021
2e8d7fe
Add root prefix to eks container file read
NathanielRN Jul 23, 2021
87863eb
Eks fixes and add helpful docstring link
NathanielRN Jul 23, 2021
d6ad808
FileNotFoundError and allow either empty container id or cluster name…
NathanielRN Jul 23, 2021
480c112
Less redundancy with general exceptions
NathanielRN Jul 23, 2021
58445b8
Update test to reflect eks with only 1 open
NathanielRN Jul 23, 2021
9cd22df
Python strings need to have backslash escaped
NathanielRN Jul 23, 2021
530a91b
lazy loading for log messages
NathanielRN Jul 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
lzchen marked this conversation as resolved.
Show resolved Hide resolved
TracerProvider(
resource=get_aggregated_resources(
lzchen marked this conversation as resolved.
Show resolved Hide resolved
[
AwsEc2ResourceDetector(),
]
),
)
)

Refer to each detectors' docstring to determine any possible requirements for that
detector.

References
lzchen marked this conversation as resolved.
Show resolved Hide resolved
----------

* `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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: opentelemetry.sdk.extension.aws.resources plural to match opentelemetry.sdk.resources? Not a big deal though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I didn't even notice that, actually I did resource because that is the folder it is in on the specifications

Also in java we put it under resource

And yet JavaScript, Go and PHP all put it under a folder called detectors/ 😅

I guess I'll defer to the SIG here, as GCP and other vendors will probably add their resource Detectors soon... maybe we should even move the SDK import path? 😓 Although I imagine that will be hard after 1.0.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We aren't at 1.0 yet so nows the time to decide on an import path :D. But again, not a blocker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I meant we should move opentelemetry.sdk.resources to opentelemetry.sdk.resource to match the spec 🙂

I actually like .resource because there is only 1 resource: namespace in the attributes. Even if you add multiple ResourceDetectors you're still setting attributes in 1 namespace...

I'll leave it for now and if we need to we'll change it before this package goes 1.0 😄. Shouldn't be a big deal to do "both" if we need to as well later!

#
# 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 (
NathanielRN marked this conversation as resolved.
Show resolved Hide resolved
AwsLambdaResourceDetector,
NathanielRN marked this conversation as resolved.
Show resolved Hide resolved
)
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__ = [
NathanielRN marked this conversation as resolved.
Show resolved Hide resolved
"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"],
lzchen marked this conversation as resolved.
Show resolved Hide resolved
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:
lzchen marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm it would have been nice if this was handled in the ResourceDetector super class so the implementations didn't have to duplicate it every time, but too late now I think 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a really good point, do you mean something like this on opentelemetry-python-core?:

class ResourceDetector(abc.ABC):
    def __init__(self, raise_on_error=False):
        self.raise_on_error = raise_on_error

    @abc.abstractmethod
    def _detect(self) -> "Resource":
        raise NotImplementedError()

    def detect(self) -> "Resource":
        try:
            self._detect()
        except NotImplementedError as exception:
            raise exception
        # pylint: disable=broad-except
        except Exception as exception:
            if self.raise_on_error:
                raise exception
            
            logger.warning(f"{self.__class__.__name__} failed: {exception}")
            return Resource.get_empty()

I think that would definitely be worth it, maybe we can add it in a minor release for now and support (but deprecate) the existing method?

If this sounds like it makes sense I can open a small PR with this change on core! 🙂

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'll be better to catch this specific type of exception below than to have nested general exceptions caught.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand you correctly, you think it would be better than we catch FileNotFoundError specifically? That makes sense to me! I'll update it here and in the eks.py file.

"/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