gcp.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import functools
  2. import sys
  3. from copy import deepcopy
  4. from datetime import datetime, timedelta, timezone
  5. from os import environ
  6. import sentry_sdk
  7. from sentry_sdk.api import continue_trace
  8. from sentry_sdk.consts import OP
  9. from sentry_sdk.integrations import Integration
  10. from sentry_sdk.integrations._wsgi_common import _filter_headers
  11. from sentry_sdk.scope import should_send_default_pii
  12. from sentry_sdk.tracing import TransactionSource
  13. from sentry_sdk.utils import (
  14. AnnotatedValue,
  15. capture_internal_exceptions,
  16. event_from_exception,
  17. logger,
  18. TimeoutThread,
  19. reraise,
  20. )
  21. from typing import TYPE_CHECKING
  22. # Constants
  23. TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry
  24. MILLIS_TO_SECONDS = 1000.0
  25. if TYPE_CHECKING:
  26. from typing import Any
  27. from typing import TypeVar
  28. from typing import Callable
  29. from typing import Optional
  30. from sentry_sdk._types import EventProcessor, Event, Hint
  31. F = TypeVar("F", bound=Callable[..., Any])
  32. def _wrap_func(func: "F") -> "F":
  33. @functools.wraps(func)
  34. def sentry_func(
  35. functionhandler: "Any", gcp_event: "Any", *args: "Any", **kwargs: "Any"
  36. ) -> "Any":
  37. client = sentry_sdk.get_client()
  38. integration = client.get_integration(GcpIntegration)
  39. if integration is None:
  40. return func(functionhandler, gcp_event, *args, **kwargs)
  41. configured_time = environ.get("FUNCTION_TIMEOUT_SEC")
  42. if not configured_time:
  43. logger.debug(
  44. "The configured timeout could not be fetched from Cloud Functions configuration."
  45. )
  46. return func(functionhandler, gcp_event, *args, **kwargs)
  47. configured_time = int(configured_time)
  48. initial_time = datetime.now(timezone.utc)
  49. with sentry_sdk.isolation_scope() as scope:
  50. with capture_internal_exceptions():
  51. scope.clear_breadcrumbs()
  52. scope.add_event_processor(
  53. _make_request_event_processor(
  54. gcp_event, configured_time, initial_time
  55. )
  56. )
  57. scope.set_tag("gcp_region", environ.get("FUNCTION_REGION"))
  58. timeout_thread = None
  59. if (
  60. integration.timeout_warning
  61. and configured_time > TIMEOUT_WARNING_BUFFER
  62. ):
  63. waiting_time = configured_time - TIMEOUT_WARNING_BUFFER
  64. timeout_thread = TimeoutThread(
  65. waiting_time,
  66. configured_time,
  67. isolation_scope=scope,
  68. current_scope=sentry_sdk.get_current_scope(),
  69. )
  70. # Starting the thread to raise timeout warning exception
  71. timeout_thread.start()
  72. headers = {}
  73. if hasattr(gcp_event, "headers"):
  74. headers = gcp_event.headers
  75. transaction = continue_trace(
  76. headers,
  77. op=OP.FUNCTION_GCP,
  78. name=environ.get("FUNCTION_NAME", ""),
  79. source=TransactionSource.COMPONENT,
  80. origin=GcpIntegration.origin,
  81. )
  82. sampling_context = {
  83. "gcp_env": {
  84. "function_name": environ.get("FUNCTION_NAME"),
  85. "function_entry_point": environ.get("ENTRY_POINT"),
  86. "function_identity": environ.get("FUNCTION_IDENTITY"),
  87. "function_region": environ.get("FUNCTION_REGION"),
  88. "function_project": environ.get("GCP_PROJECT"),
  89. },
  90. "gcp_event": gcp_event,
  91. }
  92. with sentry_sdk.start_transaction(
  93. transaction, custom_sampling_context=sampling_context
  94. ):
  95. try:
  96. return func(functionhandler, gcp_event, *args, **kwargs)
  97. except Exception:
  98. exc_info = sys.exc_info()
  99. sentry_event, hint = event_from_exception(
  100. exc_info,
  101. client_options=client.options,
  102. mechanism={"type": "gcp", "handled": False},
  103. )
  104. sentry_sdk.capture_event(sentry_event, hint=hint)
  105. reraise(*exc_info)
  106. finally:
  107. if timeout_thread:
  108. timeout_thread.stop()
  109. # Flush out the event queue
  110. client.flush()
  111. return sentry_func # type: ignore
  112. class GcpIntegration(Integration):
  113. identifier = "gcp"
  114. origin = f"auto.function.{identifier}"
  115. def __init__(self, timeout_warning: bool = False) -> None:
  116. self.timeout_warning = timeout_warning
  117. @staticmethod
  118. def setup_once() -> None:
  119. import __main__ as gcp_functions
  120. if not hasattr(gcp_functions, "worker_v1"):
  121. logger.warning(
  122. "GcpIntegration currently supports only Python 3.7 runtime environment."
  123. )
  124. return
  125. worker1 = gcp_functions.worker_v1
  126. worker1.FunctionHandler.invoke_user_function = _wrap_func(
  127. worker1.FunctionHandler.invoke_user_function
  128. )
  129. def _make_request_event_processor(
  130. gcp_event: "Any", configured_timeout: "Any", initial_time: "Any"
  131. ) -> "EventProcessor":
  132. def event_processor(event: "Event", hint: "Hint") -> "Optional[Event]":
  133. final_time = datetime.now(timezone.utc)
  134. time_diff = final_time - initial_time
  135. execution_duration_in_millis = time_diff / timedelta(milliseconds=1)
  136. extra = event.setdefault("extra", {})
  137. extra["google cloud functions"] = {
  138. "function_name": environ.get("FUNCTION_NAME"),
  139. "function_entry_point": environ.get("ENTRY_POINT"),
  140. "function_identity": environ.get("FUNCTION_IDENTITY"),
  141. "function_region": environ.get("FUNCTION_REGION"),
  142. "function_project": environ.get("GCP_PROJECT"),
  143. "execution_duration_in_millis": execution_duration_in_millis,
  144. "configured_timeout_in_seconds": configured_timeout,
  145. }
  146. extra["google cloud logs"] = {
  147. "url": _get_google_cloud_logs_url(final_time),
  148. }
  149. request = event.get("request", {})
  150. request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME"))
  151. if hasattr(gcp_event, "method"):
  152. request["method"] = gcp_event.method
  153. if hasattr(gcp_event, "query_string"):
  154. request["query_string"] = gcp_event.query_string.decode("utf-8")
  155. if hasattr(gcp_event, "headers"):
  156. request["headers"] = _filter_headers(gcp_event.headers)
  157. if should_send_default_pii():
  158. if hasattr(gcp_event, "data"):
  159. request["data"] = gcp_event.data
  160. else:
  161. if hasattr(gcp_event, "data"):
  162. # Unfortunately couldn't find a way to get structured body from GCP
  163. # event. Meaning every body is unstructured to us.
  164. request["data"] = AnnotatedValue.removed_because_raw_data()
  165. event["request"] = deepcopy(request)
  166. return event
  167. return event_processor
  168. def _get_google_cloud_logs_url(final_time: "datetime") -> str:
  169. """
  170. Generates a Google Cloud Logs console URL based on the environment variables
  171. Arguments:
  172. final_time {datetime} -- Final time
  173. Returns:
  174. str -- Google Cloud Logs Console URL to logs.
  175. """
  176. hour_ago = final_time - timedelta(hours=1)
  177. formatstring = "%Y-%m-%dT%H:%M:%SZ"
  178. url = (
  179. "https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function"
  180. "%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false"
  181. "&timestamp={timestamp_end}&customFacets=&limitCustomFacetWidth=true"
  182. "&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}"
  183. "&interval=PT1H&scrollTimestamp={timestamp_end}"
  184. ).format(
  185. project=environ.get("GCP_PROJECT"),
  186. function_name=environ.get("FUNCTION_NAME"),
  187. region=environ.get("FUNCTION_REGION"),
  188. timestamp_end=final_time.strftime(formatstring),
  189. timestamp_start=hour_ago.strftime(formatstring),
  190. )
  191. return url