cloud_resource_context.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import json
  2. import urllib3
  3. from sentry_sdk.integrations import Integration
  4. from sentry_sdk.api import set_context
  5. from sentry_sdk.utils import logger
  6. from typing import TYPE_CHECKING
  7. if TYPE_CHECKING:
  8. from typing import Dict
  9. CONTEXT_TYPE = "cloud_resource"
  10. HTTP_TIMEOUT = 2.0
  11. AWS_METADATA_HOST = "169.254.169.254"
  12. AWS_TOKEN_URL = "http://{}/latest/api/token".format(AWS_METADATA_HOST)
  13. AWS_METADATA_URL = "http://{}/latest/dynamic/instance-identity/document".format(
  14. AWS_METADATA_HOST
  15. )
  16. GCP_METADATA_HOST = "metadata.google.internal"
  17. GCP_METADATA_URL = "http://{}/computeMetadata/v1/?recursive=true".format(
  18. GCP_METADATA_HOST
  19. )
  20. class CLOUD_PROVIDER: # noqa: N801
  21. """
  22. Name of the cloud provider.
  23. see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/
  24. """
  25. ALIBABA = "alibaba_cloud"
  26. AWS = "aws"
  27. AZURE = "azure"
  28. GCP = "gcp"
  29. IBM = "ibm_cloud"
  30. TENCENT = "tencent_cloud"
  31. class CLOUD_PLATFORM: # noqa: N801
  32. """
  33. The cloud platform.
  34. see https://opentelemetry.io/docs/reference/specification/resource/semantic_conventions/cloud/
  35. """
  36. AWS_EC2 = "aws_ec2"
  37. GCP_COMPUTE_ENGINE = "gcp_compute_engine"
  38. class CloudResourceContextIntegration(Integration):
  39. """
  40. Adds cloud resource context to the Senty scope
  41. """
  42. identifier = "cloudresourcecontext"
  43. cloud_provider = ""
  44. aws_token = ""
  45. http = urllib3.PoolManager(timeout=HTTP_TIMEOUT)
  46. gcp_metadata = None
  47. def __init__(self, cloud_provider: str = "") -> None:
  48. CloudResourceContextIntegration.cloud_provider = cloud_provider
  49. @classmethod
  50. def _is_aws(cls) -> bool:
  51. try:
  52. r = cls.http.request(
  53. "PUT",
  54. AWS_TOKEN_URL,
  55. headers={"X-aws-ec2-metadata-token-ttl-seconds": "60"},
  56. )
  57. if r.status != 200:
  58. return False
  59. cls.aws_token = r.data.decode()
  60. return True
  61. except urllib3.exceptions.TimeoutError:
  62. logger.debug(
  63. "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT
  64. )
  65. return False
  66. except Exception as e:
  67. logger.debug("Error checking AWS metadata service: %s", str(e))
  68. return False
  69. @classmethod
  70. def _get_aws_context(cls) -> "Dict[str, str]":
  71. ctx = {
  72. "cloud.provider": CLOUD_PROVIDER.AWS,
  73. "cloud.platform": CLOUD_PLATFORM.AWS_EC2,
  74. }
  75. try:
  76. r = cls.http.request(
  77. "GET",
  78. AWS_METADATA_URL,
  79. headers={"X-aws-ec2-metadata-token": cls.aws_token},
  80. )
  81. if r.status != 200:
  82. return ctx
  83. data = json.loads(r.data.decode("utf-8"))
  84. try:
  85. ctx["cloud.account.id"] = data["accountId"]
  86. except Exception:
  87. pass
  88. try:
  89. ctx["cloud.availability_zone"] = data["availabilityZone"]
  90. except Exception:
  91. pass
  92. try:
  93. ctx["cloud.region"] = data["region"]
  94. except Exception:
  95. pass
  96. try:
  97. ctx["host.id"] = data["instanceId"]
  98. except Exception:
  99. pass
  100. try:
  101. ctx["host.type"] = data["instanceType"]
  102. except Exception:
  103. pass
  104. except urllib3.exceptions.TimeoutError:
  105. logger.debug(
  106. "AWS metadata service timed out after %s seconds", HTTP_TIMEOUT
  107. )
  108. except Exception as e:
  109. logger.debug("Error fetching AWS metadata: %s", str(e))
  110. return ctx
  111. @classmethod
  112. def _is_gcp(cls) -> bool:
  113. try:
  114. r = cls.http.request(
  115. "GET",
  116. GCP_METADATA_URL,
  117. headers={"Metadata-Flavor": "Google"},
  118. )
  119. if r.status != 200:
  120. return False
  121. cls.gcp_metadata = json.loads(r.data.decode("utf-8"))
  122. return True
  123. except urllib3.exceptions.TimeoutError:
  124. logger.debug(
  125. "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT
  126. )
  127. return False
  128. except Exception as e:
  129. logger.debug("Error checking GCP metadata service: %s", str(e))
  130. return False
  131. @classmethod
  132. def _get_gcp_context(cls) -> "Dict[str, str]":
  133. ctx = {
  134. "cloud.provider": CLOUD_PROVIDER.GCP,
  135. "cloud.platform": CLOUD_PLATFORM.GCP_COMPUTE_ENGINE,
  136. }
  137. try:
  138. if cls.gcp_metadata is None:
  139. r = cls.http.request(
  140. "GET",
  141. GCP_METADATA_URL,
  142. headers={"Metadata-Flavor": "Google"},
  143. )
  144. if r.status != 200:
  145. return ctx
  146. cls.gcp_metadata = json.loads(r.data.decode("utf-8"))
  147. try:
  148. ctx["cloud.account.id"] = cls.gcp_metadata["project"]["projectId"]
  149. except Exception:
  150. pass
  151. try:
  152. ctx["cloud.availability_zone"] = cls.gcp_metadata["instance"][
  153. "zone"
  154. ].split("/")[-1]
  155. except Exception:
  156. pass
  157. try:
  158. # only populated in google cloud run
  159. ctx["cloud.region"] = cls.gcp_metadata["instance"]["region"].split("/")[
  160. -1
  161. ]
  162. except Exception:
  163. pass
  164. try:
  165. ctx["host.id"] = cls.gcp_metadata["instance"]["id"]
  166. except Exception:
  167. pass
  168. except urllib3.exceptions.TimeoutError:
  169. logger.debug(
  170. "GCP metadata service timed out after %s seconds", HTTP_TIMEOUT
  171. )
  172. except Exception as e:
  173. logger.debug("Error fetching GCP metadata: %s", str(e))
  174. return ctx
  175. @classmethod
  176. def _get_cloud_provider(cls) -> str:
  177. if cls._is_aws():
  178. return CLOUD_PROVIDER.AWS
  179. if cls._is_gcp():
  180. return CLOUD_PROVIDER.GCP
  181. return ""
  182. @classmethod
  183. def _get_cloud_resource_context(cls) -> "Dict[str, str]":
  184. cloud_provider = (
  185. cls.cloud_provider
  186. if cls.cloud_provider != ""
  187. else CloudResourceContextIntegration._get_cloud_provider()
  188. )
  189. if cloud_provider in context_getters.keys():
  190. return context_getters[cloud_provider]()
  191. return {}
  192. @staticmethod
  193. def setup_once() -> None:
  194. cloud_provider = CloudResourceContextIntegration.cloud_provider
  195. unsupported_cloud_provider = (
  196. cloud_provider != "" and cloud_provider not in context_getters.keys()
  197. )
  198. if unsupported_cloud_provider:
  199. logger.warning(
  200. "Invalid value for cloud_provider: %s (must be in %s). Falling back to autodetection...",
  201. CloudResourceContextIntegration.cloud_provider,
  202. list(context_getters.keys()),
  203. )
  204. context = CloudResourceContextIntegration._get_cloud_resource_context()
  205. if context != {}:
  206. set_context(CONTEXT_TYPE, context)
  207. # Map with the currently supported cloud providers
  208. # mapping to functions extracting the context
  209. context_getters = {
  210. CLOUD_PROVIDER.AWS: CloudResourceContextIntegration._get_aws_context,
  211. CLOUD_PROVIDER.GCP: CloudResourceContextIntegration._get_gcp_context,
  212. }