1616import functools
1717import inspect
1818import os
19+ import re
20+ import sys
1921
2022from typing import Any , Awaitable , Callable , Dict , Tuple , Union
2123
2224from cloudevents .http import from_http
2325from cloudevents .http .event import CloudEvent
2426
25- from functions_framework import _function_registry
27+ from functions_framework import _function_registry , execution_id
2628from functions_framework .exceptions import (
2729 FunctionsFrameworkException ,
2830 MissingSourceException ,
5153_FUNCTION_STATUS_HEADER_FIELD = "X-Google-Status"
5254_CRASH = "crash"
5355
56+
57+ async def _crash_handler (request , exc ): # pragma: no cover
58+ headers = {_FUNCTION_STATUS_HEADER_FIELD : _CRASH }
59+ return Response ("Internal Server Error" , status_code = 500 , headers = headers )
60+
5461CloudEventFunction = Callable [[CloudEvent ], Union [None , Awaitable [None ]]]
5562HTTPFunction = Callable [[Request ], Union [HTTPResponse , Awaitable [HTTPResponse ]]]
5663
@@ -96,38 +103,46 @@ def wrapper(*args, **kwargs):
96103 return wrapper
97104
98105
99- async def _crash_handler (request , exc ):
100- headers = {_FUNCTION_STATUS_HEADER_FIELD : _CRASH }
101- return Response (f"Internal Server Error: { exc } " , status_code = 500 , headers = headers )
102-
103-
104- def _http_func_wrapper (function , is_async ):
106+ def _http_func_wrapper (function , is_async , enable_id_logging = False ):
107+ @execution_id .set_execution_context_async (enable_id_logging )
105108 @functools .wraps (function )
106109 async def handler (request ):
107- if is_async :
108- result = await function (request )
109- else :
110- # TODO: Use asyncio.to_thread when we drop Python 3.8 support
111- # Python 3.8 compatible version of asyncio.to_thread
112- loop = asyncio .get_event_loop ()
113- result = await loop .run_in_executor (None , function , request )
114- if isinstance (result , str ):
115- return Response (result )
116- elif isinstance (result , dict ):
117- return JSONResponse (result )
118- elif isinstance (result , tuple ) and len (result ) == 2 :
119- # Support Flask-style tuple response
120- content , status_code = result
121- return Response (content , status_code = status_code )
122- elif result is None :
123- raise HTTPException (status_code = 500 , detail = "No response returned" )
124- else :
125- return result
110+ try :
111+ if is_async :
112+ result = await function (request )
113+ else :
114+ # TODO: Use asyncio.to_thread when we drop Python 3.8 support
115+ # Python 3.8 compatible version of asyncio.to_thread
116+ loop = asyncio .get_event_loop ()
117+ result = await loop .run_in_executor (None , function , request )
118+ if isinstance (result , str ):
119+ return Response (result )
120+ elif isinstance (result , dict ):
121+ return JSONResponse (result )
122+ elif isinstance (result , tuple ) and len (result ) == 2 :
123+ # Support Flask-style tuple response
124+ content , status_code = result
125+ if isinstance (content , dict ):
126+ return JSONResponse (content , status_code = status_code )
127+ else :
128+ return Response (content , status_code = status_code )
129+ elif result is None :
130+ raise HTTPException (status_code = 500 , detail = "No response returned" )
131+ else :
132+ return result
133+ except Exception : # pragma: no cover
134+ # Log the exception while context is still active
135+ # The traceback will be printed to stderr which goes through LoggingHandlerAddExecutionId
136+ import sys
137+ import traceback
138+ traceback .print_exc (file = sys .stderr )
139+ raise
126140
127141 return handler
128142
129143
130- def _cloudevent_func_wrapper (function , is_async ):
144+ def _cloudevent_func_wrapper (function , is_async , enable_id_logging = False ):
145+ @execution_id .set_execution_context_async (enable_id_logging )
131146 @functools .wraps (function )
132147 async def handler (request ):
133148 data = await request .body ()
@@ -138,14 +153,23 @@ async def handler(request):
138153 raise HTTPException (
139154 400 , detail = f"Bad Request: Got CloudEvent exception: { repr (e )} "
140155 )
141- if is_async :
142- await function (event )
143- else :
144- # TODO: Use asyncio.to_thread when we drop Python 3.8 support
145- # Python 3.8 compatible version of asyncio.to_thread
146- loop = asyncio .get_event_loop ()
147- await loop .run_in_executor (None , function , event )
148- return Response ("OK" )
156+
157+ try :
158+ if is_async :
159+ await function (event )
160+ else :
161+ # TODO: Use asyncio.to_thread when we drop Python 3.8 support
162+ # Python 3.8 compatible version of asyncio.to_thread
163+ loop = asyncio .get_event_loop ()
164+ await loop .run_in_executor (None , function , event )
165+ return Response ("OK" )
166+ except Exception : # pragma: no cover
167+ # Log the exception while context is still active
168+ # The traceback will be printed to stderr which goes through LoggingHandlerAddExecutionId
169+ import sys
170+ import traceback
171+ traceback .print_exc (file = sys .stderr )
172+ raise
149173
150174 return handler
151175
@@ -154,6 +178,32 @@ async def _handle_not_found(request: Request):
154178 raise HTTPException (status_code = 404 , detail = "Not Found" )
155179
156180
181+ def _enable_execution_id_logging ():
182+ # Based on distutils.util.strtobool
183+ truthy_values = ("y" , "yes" , "t" , "true" , "on" , "1" )
184+ env_var_value = os .environ .get ("LOG_EXECUTION_ID" )
185+ return env_var_value in truthy_values
186+
187+
188+ def _configure_app_execution_id_logging ():
189+ # Logging needs to be configured before app logger is accessed
190+ import logging .config
191+ import logging
192+
193+ # Configure root logger to use our custom handler
194+ root_logger = logging .getLogger ()
195+ root_logger .setLevel (logging .INFO )
196+
197+ # Remove existing handlers
198+ for handler in root_logger .handlers [:]:
199+ root_logger .removeHandler (handler )
200+
201+ # Add our custom handler that adds execution ID
202+ handler = logging .StreamHandler (execution_id .LoggingHandlerAddExecutionId (sys .stderr ))
203+ handler .setLevel (logging .NOTSET )
204+ root_logger .addHandler (handler )
205+
206+
157207def create_asgi_app (target = None , source = None , signature_type = None ):
158208 """Create an ASGI application for the function.
159209
@@ -175,14 +225,19 @@ def create_asgi_app(target=None, source=None, signature_type=None):
175225 )
176226
177227 source_module , spec = _function_registry .load_function_module (source )
228+
229+ enable_id_logging = _enable_execution_id_logging ()
230+ if enable_id_logging :
231+ _configure_app_execution_id_logging ()
232+
178233 spec .loader .exec_module (source_module )
179234 function = _function_registry .get_user_function (source , source_module , target )
180235 signature_type = _function_registry .get_func_signature_type (target , signature_type )
181236
182237 is_async = inspect .iscoroutinefunction (function )
183238 routes = []
184239 if signature_type == _function_registry .HTTP_SIGNATURE_TYPE :
185- http_handler = _http_func_wrapper (function , is_async )
240+ http_handler = _http_func_wrapper (function , is_async , enable_id_logging )
186241 routes .append (
187242 Route (
188243 "/" ,
@@ -202,7 +257,7 @@ def create_asgi_app(target=None, source=None, signature_type=None):
202257 )
203258 )
204259 elif signature_type == _function_registry .CLOUDEVENT_SIGNATURE_TYPE :
205- cloudevent_handler = _cloudevent_func_wrapper (function , is_async )
260+ cloudevent_handler = _cloudevent_func_wrapper (function , is_async , enable_id_logging )
206261 routes .append (
207262 Route ("/{path:path}" , endpoint = cloudevent_handler , methods = ["POST" ])
208263 )
@@ -225,6 +280,10 @@ def create_asgi_app(target=None, source=None, signature_type=None):
225280 500 : _crash_handler ,
226281 }
227282 app = Starlette (routes = routes , exception_handlers = exception_handlers )
283+
284+ # Apply ASGI middleware for execution ID
285+ app = execution_id .AsgiMiddleware (app )
286+
228287 return app
229288
230289
0 commit comments