Skip to content

feat(cloud): Adding Cloud Resource Context #1882

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

Merged
merged 41 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
259ce48
Initial version of getting cloud context from AWS.
antonpirker Jan 31, 2023
8af586d
Fixing guessing if we are in AWS
antonpirker Feb 1, 2023
d2f7f8b
Added option for specifying the cloud provider, to prevent autodetection
antonpirker Feb 6, 2023
fb23b64
Added GCP support
antonpirker Feb 6, 2023
3d64837
Added some debug output
antonpirker Feb 6, 2023
bb32664
Better logging
antonpirker Feb 6, 2023
f248f1f
Check for correct status response
antonpirker Feb 7, 2023
444aa27
Cleanup
antonpirker Feb 7, 2023
1e4748c
Some doc strings
antonpirker Feb 7, 2023
fd948aa
Added GCE host id
antonpirker Feb 7, 2023
0fece86
Added more data to AWS context
antonpirker Feb 7, 2023
f0d539f
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 7, 2023
0ec6c60
Fix linting error
antonpirker Feb 7, 2023
b4bafba
Added tests (and fixed a discovered bug)
antonpirker Feb 8, 2023
8abbdcd
Added cloudcontext to test suite
antonpirker Feb 8, 2023
48e0c09
Removed Python 2.7
antonpirker Feb 8, 2023
f0c9299
Renamed cloud context to cloud_resource context for better descriptio…
antonpirker Feb 8, 2023
cd5f890
Added missing files
antonpirker Feb 8, 2023
96dea13
Removed stuff that should not be there.
antonpirker Feb 8, 2023
845786d
Cleanup
antonpirker Feb 8, 2023
f08b943
Cleanup
antonpirker Feb 8, 2023
8157ec2
Renamed cloudresourceconetxt to could_resource_context
antonpirker Feb 10, 2023
9b5a3e7
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 10, 2023
f52de3c
Do not use separate fields for availability zone of GCP and AWS
antonpirker Feb 15, 2023
dd7f12f
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 15, 2023
f3d6bed
Fixed test after renaming context
antonpirker Feb 15, 2023
5d49e4a
Merge branch 'antonpirker/1820-cloud-context' of github.com:getsentry…
antonpirker Feb 15, 2023
37586c5
Renamed keys to make ingestion possible
antonpirker Feb 15, 2023
610f918
Revert "Renamed keys to make ingestion possible"
antonpirker Feb 16, 2023
b089dce
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 16, 2023
f6d0489
Fixed typo
antonpirker Feb 16, 2023
3ee1d8c
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 17, 2023
2d0f3f3
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 17, 2023
7d08de7
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 17, 2023
ab90999
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 17, 2023
490fd91
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 21, 2023
a032e75
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 21, 2023
fc2d051
Made it support Python 2
antonpirker Feb 22, 2023
3e6ecb8
Merge branch 'antonpirker/1820-cloud-context' of github.com:getsentry…
antonpirker Feb 22, 2023
12b7289
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 22, 2023
716ed20
Merge branch 'master' into antonpirker/1820-cloud-context
antonpirker Feb 22, 2023
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
73 changes: 73 additions & 0 deletions .github/workflows/test-integration-cloud_resource_context.yml
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
258 changes: 258 additions & 0 deletions sentry_sdk/integrations/cloud_resource_context.py
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.
Loading