-
Notifications
You must be signed in to change notification settings - Fork 515
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloud): Adding Cloud Resource Context (#1882)
* Initial version of getting cloud context from AWS and GCP.
- Loading branch information
1 parent
2d24560
commit 5306eab
Showing
5 changed files
with
740 additions
and
0 deletions.
There are no files selected for viewing
73 changes: 73 additions & 0 deletions
73
.github/workflows/test-integration-cloud_resource_context.yml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
name: Test cloud_resource_context | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
- release/** | ||
|
||
pull_request: | ||
|
||
# Cancel in progress workflows on pull_requests. | ||
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} | ||
cancel-in-progress: true | ||
|
||
permissions: | ||
contents: read | ||
|
||
env: | ||
BUILD_CACHE_KEY: ${{ github.sha }} | ||
CACHED_BUILD_PATHS: | | ||
${{ github.workspace }}/dist-serverless | ||
jobs: | ||
test: | ||
name: cloud_resource_context, python ${{ matrix.python-version }}, ${{ matrix.os }} | ||
runs-on: ${{ matrix.os }} | ||
timeout-minutes: 45 | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11"] | ||
# python3.6 reached EOL and is no longer being supported on | ||
# new versions of hosted runners on Github Actions | ||
# ubuntu-20.04 is the last version that supported python3.6 | ||
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877 | ||
os: [ubuntu-20.04] | ||
|
||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/setup-python@v4 | ||
with: | ||
python-version: ${{ matrix.python-version }} | ||
|
||
- name: Setup Test Env | ||
run: | | ||
pip install codecov "tox>=3,<4" | ||
- name: Test cloud_resource_context | ||
timeout-minutes: 45 | ||
shell: bash | ||
run: | | ||
set -x # print commands that are executed | ||
coverage erase | ||
./scripts/runtox.sh "${{ matrix.python-version }}-cloud_resource_context" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch | ||
coverage combine .coverage* | ||
coverage xml -i | ||
codecov --file coverage.xml | ||
check_required_tests: | ||
name: All cloud_resource_context tests passed or skipped | ||
needs: test | ||
# Always run this, even if a dependent job failed | ||
if: always() | ||
runs-on: ubuntu-20.04 | ||
steps: | ||
- name: Check for failures | ||
if: contains(needs.test.result, 'failure') | ||
run: | | ||
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
import json | ||
import urllib3 # type: ignore | ||
|
||
from sentry_sdk.integrations import Integration | ||
from sentry_sdk.api import set_context | ||
from sentry_sdk.utils import logger | ||
|
||
from sentry_sdk._types import MYPY | ||
|
||
if MYPY: | ||
from typing import Dict | ||
|
||
|
||
CONTEXT_TYPE = "cloud_resource" | ||
|
||
AWS_METADATA_HOST = "169.254.169.254" | ||
AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST) | ||
AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format( | ||
AWS_METADATA_HOST | ||
) | ||
|
||
GCP_METADATA_HOST = "metadata.google.internal" | ||
GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format( | ||
GCP_METADATA_HOST | ||
) | ||
|
||
|
||
class CLOUD_PROVIDER: # noqa: N801 | ||
""" | ||
Name of the cloud provider. | ||
see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ | ||
""" | ||
|
||
ALIBABA = "alibaba_cloud" | ||
AWS = "aws" | ||
AZURE = "azure" | ||
GCP = "gcp" | ||
IBM = "ibm_cloud" | ||
TENCENT = "tencent_cloud" | ||
|
||
|
||
class CLOUD_PLATFORM: # noqa: N801 | ||
""" | ||
The cloud platform. | ||
see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/ | ||
""" | ||
|
||
AWS_EC2 = "aws_ec2" | ||
GCP_COMPUTE_ENGINE = "gcp_compute_engine" | ||
|
||
|
||
class CloudResourceContextIntegration(Integration): | ||
""" | ||
Adds cloud resource context to the Senty scope | ||
""" | ||
|
||
identifier = "cloudresourcecontext" | ||
|
||
cloud_provider = "" | ||
|
||
aws_token = "" | ||
http = urllib3.PoolManager() | ||
|
||
gcp_metadata = None | ||
|
||
def __init__(self, cloud_provider=""): | ||
# type: (str) -> None | ||
CloudResourceContextIntegration.cloud_provider = cloud_provider | ||
|
||
@classmethod | ||
def _is_aws(cls): | ||
# type: () -> bool | ||
try: | ||
r = cls.http.request( | ||
"PUT", | ||
AWS_TOKEN_URL, | ||
headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"}, | ||
) | ||
|
||
if r.status != 200: | ||
return False | ||
|
||
cls.aws_token = r.data | ||
return True | ||
|
||
except Exception: | ||
return False | ||
|
||
@classmethod | ||
def _get_aws_context(cls): | ||
# type: () -> Dict[str, str] | ||
ctx = { | ||
"cloud.provider": CLOUD_PROVIDER.AWS, | ||
"cloud.platform": CLOUD_PLATFORM.AWS_EC2, | ||
} | ||
|
||
try: | ||
r = cls.http.request( | ||
"GET", | ||
AWS_METADATA_URL, | ||
headers={"X-aws-ec2-metadata-token": cls.aws_token}, | ||
) | ||
|
||
if r.status != 200: | ||
return ctx | ||
|
||
data = json.loads(r.data.decode("utf-8")) | ||
|
||
try: | ||
ctx["cloud.account.id"] = data["accountId"] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["cloud.availability_zone"] = data["availabilityZone"] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["cloud.region"] = data["region"] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["host.id"] = data["instanceId"] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["host.type"] = data["instanceType"] | ||
except Exception: | ||
pass | ||
|
||
except Exception: | ||
pass | ||
|
||
return ctx | ||
|
||
@classmethod | ||
def _is_gcp(cls): | ||
# type: () -> bool | ||
try: | ||
r = cls.http.request( | ||
"GET", | ||
GCP_METADATA_URL, | ||
headers={"Metadata-Flavor": "Google"}, | ||
) | ||
|
||
if r.status != 200: | ||
return False | ||
|
||
cls.gcp_metadata = json.loads(r.data.decode("utf-8")) | ||
return True | ||
|
||
except Exception: | ||
return False | ||
|
||
@classmethod | ||
def _get_gcp_context(cls): | ||
# type: () -> Dict[str, str] | ||
ctx = { | ||
"cloud.provider": CLOUD_PROVIDER.GCP, | ||
"cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE, | ||
} | ||
|
||
try: | ||
if cls.gcp_metadata is None: | ||
r = cls.http.request( | ||
"GET", | ||
GCP_METADATA_URL, | ||
headers={"Metadata-Flavor": "Google"}, | ||
) | ||
|
||
if r.status != 200: | ||
return ctx | ||
|
||
cls.gcp_metadata = json.loads(r.data.decode("utf-8")) | ||
|
||
try: | ||
ctx["cloud.account.id"] = cls.gcp_metadata["project"]["projectId"] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["cloud.availability_zone"] = cls.gcp_metadata["instance"][ | ||
"zone" | ||
].split("/")[-1] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
# only populated in google cloud run | ||
ctx["cloud.region"] = cls.gcp_metadata["instance"]["region"].split("/")[ | ||
-1 | ||
] | ||
except Exception: | ||
pass | ||
|
||
try: | ||
ctx["host.id"] = cls.gcp_metadata["instance"]["id"] | ||
except Exception: | ||
pass | ||
|
||
except Exception: | ||
pass | ||
|
||
return ctx | ||
|
||
@classmethod | ||
def _get_cloud_provider(cls): | ||
# type: () -> str | ||
if cls._is_aws(): | ||
return CLOUD_PROVIDER.AWS | ||
|
||
if cls._is_gcp(): | ||
return CLOUD_PROVIDER.GCP | ||
|
||
return "" | ||
|
||
@classmethod | ||
def _get_cloud_resource_context(cls): | ||
# type: () -> Dict[str, str] | ||
cloud_provider = ( | ||
cls.cloud_provider | ||
if cls.cloud_provider != "" | ||
else CloudResourceContextIntegration._get_cloud_provider() | ||
) | ||
if cloud_provider in context_getters.keys(): | ||
return context_getters[cloud_provider]() | ||
|
||
return {} | ||
|
||
@staticmethod | ||
def setup_once(): | ||
# type: () -> None | ||
cloud_provider = CloudResourceContextIntegration.cloud_provider | ||
unsupported_cloud_provider = ( | ||
cloud_provider != "" and cloud_provider not in context_getters.keys() | ||
) | ||
|
||
if unsupported_cloud_provider: | ||
logger.warning( | ||
"Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...", | ||
CloudResourceContextIntegration.cloud_provider, | ||
list(context_getters.keys()), | ||
) | ||
|
||
context = CloudResourceContextIntegration._get_cloud_resource_context() | ||
if context != {}: | ||
set_context(CONTEXT_TYPE, context) | ||
|
||
|
||
# Map with the currently supported cloud providers | ||
# mapping to functions extracting the context | ||
context_getters = { | ||
CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context, | ||
CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context, | ||
} |
Empty file.
Oops, something went wrong.