Skip to content

Commit 3b37cb5

Browse files
authored
Added a new integration for Google Cloud Functions (#785)
1 parent fb3a4c8 commit 3b37cb5

File tree

3 files changed

+572
-0
lines changed

3 files changed

+572
-0
lines changed

sentry_sdk/integrations/gcp.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
from datetime import datetime, timedelta
2+
from os import environ
3+
import sys
4+
5+
from sentry_sdk.hub import Hub
6+
from sentry_sdk._compat import reraise
7+
from sentry_sdk.utils import (
8+
capture_internal_exceptions,
9+
event_from_exception,
10+
logger,
11+
TimeoutThread,
12+
)
13+
from sentry_sdk.integrations import Integration
14+
15+
from sentry_sdk._types import MYPY
16+
17+
# Constants
18+
TIMEOUT_WARNING_BUFFER = 1.5 # Buffer time required to send timeout warning to Sentry
19+
MILLIS_TO_SECONDS = 1000.0
20+
21+
if MYPY:
22+
from typing import Any
23+
from typing import TypeVar
24+
from typing import Callable
25+
from typing import Optional
26+
27+
from sentry_sdk._types import EventProcessor, Event, Hint
28+
29+
F = TypeVar("F", bound=Callable[..., Any])
30+
31+
32+
def _wrap_func(func):
33+
# type: (F) -> F
34+
def sentry_func(*args, **kwargs):
35+
# type: (*Any, **Any) -> Any
36+
37+
hub = Hub.current
38+
integration = hub.get_integration(GcpIntegration)
39+
if integration is None:
40+
return func(*args, **kwargs)
41+
42+
# If an integration is there, a client has to be there.
43+
client = hub.client # type: Any
44+
45+
configured_time = environ.get("FUNCTION_TIMEOUT_SEC")
46+
if not configured_time:
47+
logger.debug(
48+
"The configured timeout could not be fetched from Cloud Functions configuration."
49+
)
50+
return func(*args, **kwargs)
51+
52+
configured_time = int(configured_time)
53+
54+
initial_time = datetime.now()
55+
56+
with hub.push_scope() as scope:
57+
with capture_internal_exceptions():
58+
scope.clear_breadcrumbs()
59+
scope.transaction = environ.get("FUNCTION_NAME")
60+
scope.add_event_processor(
61+
_make_request_event_processor(configured_time, initial_time)
62+
)
63+
try:
64+
if (
65+
integration.timeout_warning
66+
and configured_time > TIMEOUT_WARNING_BUFFER
67+
):
68+
waiting_time = configured_time - TIMEOUT_WARNING_BUFFER
69+
70+
timeout_thread = TimeoutThread(waiting_time, configured_time)
71+
72+
# Starting the thread to raise timeout warning exception
73+
timeout_thread.start()
74+
return func(*args, **kwargs)
75+
except Exception:
76+
exc_info = sys.exc_info()
77+
event, hint = event_from_exception(
78+
exc_info,
79+
client_options=client.options,
80+
mechanism={"type": "gcp", "handled": False},
81+
)
82+
hub.capture_event(event, hint=hint)
83+
reraise(*exc_info)
84+
finally:
85+
# Flush out the event queue
86+
hub.flush()
87+
88+
return sentry_func # type: ignore
89+
90+
91+
class GcpIntegration(Integration):
92+
identifier = "gcp"
93+
94+
@staticmethod
95+
def setup_once():
96+
# type: () -> None
97+
import __main__ as gcp_functions # type: ignore
98+
99+
if not hasattr(gcp_functions, "worker_v1"):
100+
logger.warning(
101+
"GcpIntegration currently supports only Python 3.7 runtime environment."
102+
)
103+
return
104+
105+
worker1 = gcp_functions.worker_v1
106+
107+
worker1.FunctionHandler.invoke_user_function = _wrap_func(
108+
worker1.FunctionHandler.invoke_user_function
109+
)
110+
111+
112+
def _make_request_event_processor(configured_timeout, initial_time):
113+
# type: (Any, Any) -> EventProcessor
114+
115+
def event_processor(event, hint):
116+
# type: (Event, Hint) -> Optional[Event]
117+
118+
final_time = datetime.now()
119+
time_diff = final_time - initial_time
120+
121+
execution_duration_in_millis = time_diff.microseconds / MILLIS_TO_SECONDS
122+
123+
extra = event.setdefault("extra", {})
124+
extra["google cloud functions"] = {
125+
"function_name": environ.get("FUNCTION_NAME"),
126+
"function_entry_point": environ.get("ENTRY_POINT"),
127+
"function_identity": environ.get("FUNCTION_IDENTITY"),
128+
"function_region": environ.get("FUNCTION_REGION"),
129+
"function_project": environ.get("GCP_PROJECT"),
130+
"execution_duration_in_millis": execution_duration_in_millis,
131+
"configured_timeout_in_seconds": configured_timeout,
132+
}
133+
134+
extra["google cloud logs"] = {
135+
"url": _get_google_cloud_logs_url(initial_time),
136+
}
137+
138+
request = event.get("request", {})
139+
140+
request["url"] = "gcp:///{}".format(environ.get("FUNCTION_NAME"))
141+
142+
event["request"] = request
143+
144+
return event
145+
146+
return event_processor
147+
148+
149+
def _get_google_cloud_logs_url(initial_time):
150+
# type: (datetime) -> str
151+
"""
152+
Generates a Google Cloud Logs console URL based on the environment variables
153+
Arguments:
154+
initial_time {datetime} -- Initial time
155+
Returns:
156+
str -- Google Cloud Logs Console URL to logs.
157+
"""
158+
hour_ago = initial_time - timedelta(hours=1)
159+
160+
url = (
161+
"https://console.cloud.google.com/logs/viewer?project={project}&resource=cloud_function"
162+
"%2Ffunction_name%2F{function_name}%2Fregion%2F{region}&minLogLevel=0&expandAll=false"
163+
"&timestamp={initial_time}&customFacets=&limitCustomFacetWidth=true"
164+
"&dateRangeStart={timestamp_start}&dateRangeEnd={timestamp_end}"
165+
"&interval=PT1H&scrollTimestamp={timestamp_current}"
166+
).format(
167+
project=environ.get("GCP_PROJECT"),
168+
function_name=environ.get("FUNCTION_NAME"),
169+
region=environ.get("FUNCTION_REGION"),
170+
initial_time=initial_time,
171+
timestamp_start=hour_ago,
172+
timestamp_end=initial_time,
173+
timestamp_current=initial_time,
174+
)
175+
176+
return url

0 commit comments

Comments
 (0)