Skip to content

Commit 5306eab

Browse files
authored
feat(cloud): Adding Cloud Resource Context (#1882)
* Initial version of getting cloud context from AWS and GCP.
1 parent 2d24560 commit 5306eab

File tree

5 files changed

+740
-0
lines changed

5 files changed

+740
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Test cloud_resource_context
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- release/**
8+
9+
pull_request:
10+
11+
# Cancel in progress workflows on pull_requests.
12+
# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
15+
cancel-in-progress: true
16+
17+
permissions:
18+
contents: read
19+
20+
env:
21+
BUILD_CACHE_KEY: ${{ github.sha }}
22+
CACHED_BUILD_PATHS: |
23+
${{ github.workspace }}/dist-serverless
24+
25+
jobs:
26+
test:
27+
name: cloud_resource_context, python ${{ matrix.python-version }}, ${{ matrix.os }}
28+
runs-on: ${{ matrix.os }}
29+
timeout-minutes: 45
30+
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
python-version: ["3.6","3.7","3.8","3.9","3.10","3.11"]
35+
# python3.6 reached EOL and is no longer being supported on
36+
# new versions of hosted runners on Github Actions
37+
# ubuntu-20.04 is the last version that supported python3.6
38+
# see https://github.com/actions/setup-python/issues/544#issuecomment-1332535877
39+
os: [ubuntu-20.04]
40+
41+
steps:
42+
- uses: actions/checkout@v3
43+
- uses: actions/setup-python@v4
44+
with:
45+
python-version: ${{ matrix.python-version }}
46+
47+
- name: Setup Test Env
48+
run: |
49+
pip install codecov "tox>=3,<4"
50+
51+
- name: Test cloud_resource_context
52+
timeout-minutes: 45
53+
shell: bash
54+
run: |
55+
set -x # print commands that are executed
56+
coverage erase
57+
58+
./scripts/runtox.sh "${{ matrix.python-version }}-cloud_resource_context" --cov=tests --cov=sentry_sdk --cov-report= --cov-branch
59+
coverage combine .coverage*
60+
coverage xml -i
61+
codecov --file coverage.xml
62+
63+
check_required_tests:
64+
name: All cloud_resource_context tests passed or skipped
65+
needs: test
66+
# Always run this, even if a dependent job failed
67+
if: always()
68+
runs-on: ubuntu-20.04
69+
steps:
70+
- name: Check for failures
71+
if: contains(needs.test.result, 'failure')
72+
run: |
73+
echo "One of the dependent jobs have failed. You may need to re-run it." && exit 1
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import json
2+
import urllib3 # type: ignore
3+
4+
from sentry_sdk.integrations import Integration
5+
from sentry_sdk.api import set_context
6+
from sentry_sdk.utils import logger
7+
8+
from sentry_sdk._types import MYPY
9+
10+
if MYPY:
11+
from typing import Dict
12+
13+
14+
CONTEXT_TYPE = "cloud_resource"
15+
16+
AWS_METADATA_HOST = "169.254.169.254"
17+
AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST)
18+
AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format(
19+
AWS_METADATA_HOST
20+
)
21+
22+
GCP_METADATA_HOST = "metadata.google.internal"
23+
GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format(
24+
GCP_METADATA_HOST
25+
)
26+
27+
28+
class CLOUD_PROVIDER: # noqa: N801
29+
"""
30+
Name of the cloud provider.
31+
see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/
32+
"""
33+
34+
ALIBABA = "alibaba_cloud"
35+
AWS = "aws"
36+
AZURE = "azure"
37+
GCP = "gcp"
38+
IBM = "ibm_cloud"
39+
TENCENT = "tencent_cloud"
40+
41+
42+
class CLOUD_PLATFORM: # noqa: N801
43+
"""
44+
The cloud platform.
45+
see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/
46+
"""
47+
48+
AWS_EC2 = "aws_ec2"
49+
GCP_COMPUTE_ENGINE = "gcp_compute_engine"
50+
51+
52+
class CloudResourceContextIntegration(Integration):
53+
"""
54+
Adds cloud resource context to the Senty scope
55+
"""
56+
57+
identifier = "cloudresourcecontext"
58+
59+
cloud_provider = ""
60+
61+
aws_token = ""
62+
http = urllib3.PoolManager()
63+
64+
gcp_metadata = None
65+
66+
def __init__(self, cloud_provider=""):
67+
# type: (str) -> None
68+
CloudResourceContextIntegration.cloud_provider = cloud_provider
69+
70+
@classmethod
71+
def _is_aws(cls):
72+
# type: () -> bool
73+
try:
74+
r = cls.http.request(
75+
"PUT",
76+
AWS_TOKEN_URL,
77+
headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"},
78+
)
79+
80+
if r.status != 200:
81+
return False
82+
83+
cls.aws_token = r.data
84+
return True
85+
86+
except Exception:
87+
return False
88+
89+
@classmethod
90+
def _get_aws_context(cls):
91+
# type: () -> Dict[str, str]
92+
ctx = {
93+
"cloud.provider": CLOUD_PROVIDER.AWS,
94+
"cloud.platform": CLOUD_PLATFORM.AWS_EC2,
95+
}
96+
97+
try:
98+
r = cls.http.request(
99+
"GET",
100+
AWS_METADATA_URL,
101+
headers={"X-aws-ec2-metadata-token": cls.aws_token},
102+
)
103+
104+
if r.status != 200:
105+
return ctx
106+
107+
data = json.loads(r.data.decode("utf-8"))
108+
109+
try:
110+
ctx["cloud.account.id"] = data["accountId"]
111+
except Exception:
112+
pass
113+
114+
try:
115+
ctx["cloud.availability_zone"] = data["availabilityZone"]
116+
except Exception:
117+
pass
118+
119+
try:
120+
ctx["cloud.region"] = data["region"]
121+
except Exception:
122+
pass
123+
124+
try:
125+
ctx["host.id"] = data["instanceId"]
126+
except Exception:
127+
pass
128+
129+
try:
130+
ctx["host.type"] = data["instanceType"]
131+
except Exception:
132+
pass
133+
134+
except Exception:
135+
pass
136+
137+
return ctx
138+
139+
@classmethod
140+
def _is_gcp(cls):
141+
# type: () -> bool
142+
try:
143+
r = cls.http.request(
144+
"GET",
145+
GCP_METADATA_URL,
146+
headers={"Metadata-Flavor": "Google"},
147+
)
148+
149+
if r.status != 200:
150+
return False
151+
152+
cls.gcp_metadata = json.loads(r.data.decode("utf-8"))
153+
return True
154+
155+
except Exception:
156+
return False
157+
158+
@classmethod
159+
def _get_gcp_context(cls):
160+
# type: () -> Dict[str, str]
161+
ctx = {
162+
"cloud.provider": CLOUD_PROVIDER.GCP,
163+
"cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE,
164+
}
165+
166+
try:
167+
if cls.gcp_metadata is None:
168+
r = cls.http.request(
169+
"GET",
170+
GCP_METADATA_URL,
171+
headers={"Metadata-Flavor": "Google"},
172+
)
173+
174+
if r.status != 200:
175+
return ctx
176+
177+
cls.gcp_metadata = json.loads(r.data.decode("utf-8"))
178+
179+
try:
180+
ctx["cloud.account.id"] = cls.gcp_metadata["project"]["projectId"]
181+
except Exception:
182+
pass
183+
184+
try:
185+
ctx["cloud.availability_zone"] = cls.gcp_metadata["instance"][
186+
"zone"
187+
].split("/")[-1]
188+
except Exception:
189+
pass
190+
191+
try:
192+
# only populated in google cloud run
193+
ctx["cloud.region"] = cls.gcp_metadata["instance"]["region"].split("/")[
194+
-1
195+
]
196+
except Exception:
197+
pass
198+
199+
try:
200+
ctx["host.id"] = cls.gcp_metadata["instance"]["id"]
201+
except Exception:
202+
pass
203+
204+
except Exception:
205+
pass
206+
207+
return ctx
208+
209+
@classmethod
210+
def _get_cloud_provider(cls):
211+
# type: () -> str
212+
if cls._is_aws():
213+
return CLOUD_PROVIDER.AWS
214+
215+
if cls._is_gcp():
216+
return CLOUD_PROVIDER.GCP
217+
218+
return ""
219+
220+
@classmethod
221+
def _get_cloud_resource_context(cls):
222+
# type: () -> Dict[str, str]
223+
cloud_provider = (
224+
cls.cloud_provider
225+
if cls.cloud_provider != ""
226+
else CloudResourceContextIntegration._get_cloud_provider()
227+
)
228+
if cloud_provider in context_getters.keys():
229+
return context_getters[cloud_provider]()
230+
231+
return {}
232+
233+
@staticmethod
234+
def setup_once():
235+
# type: () -> None
236+
cloud_provider = CloudResourceContextIntegration.cloud_provider
237+
unsupported_cloud_provider = (
238+
cloud_provider != "" and cloud_provider not in context_getters.keys()
239+
)
240+
241+
if unsupported_cloud_provider:
242+
logger.warning(
243+
"Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...",
244+
CloudResourceContextIntegration.cloud_provider,
245+
list(context_getters.keys()),
246+
)
247+
248+
context = CloudResourceContextIntegration._get_cloud_resource_context()
249+
if context != {}:
250+
set_context(CONTEXT_TYPE, context)
251+
252+
253+
# Map with the currently supported cloud providers
254+
# mapping to functions extracting the context
255+
context_getters = {
256+
CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context,
257+
CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context,
258+
}

tests/integrations/cloud_resource_context/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)