diff --git a/azure/functions/_abc.py b/azure/functions/_abc.py index a2a1e391..56e70b2a 100644 --- a/azure/functions/_abc.py +++ b/azure/functions/_abc.py @@ -4,6 +4,7 @@ import abc import datetime import io +import threading import typing from azure.functions._thirdparty.werkzeug.datastructures import Headers @@ -102,6 +103,18 @@ def invocation_id(self) -> str: """Function invocation ID.""" pass + @property + @abc.abstractmethod + def thread_local_storage(self) -> typing.Type[threading.local]: + """Thread local storage. + + :attribute str invocation_id: + Invocation ID contained in local thread storage. + Enables logging from user threads when set to + the current context's invocation ID. + """ + pass + @property @abc.abstractmethod def function_name(self) -> str: diff --git a/azure/functions/_http_asgi.py b/azure/functions/_http_asgi.py index 52151812..67053559 100644 --- a/azure/functions/_http_asgi.py +++ b/azure/functions/_http_asgi.py @@ -56,6 +56,8 @@ def to_asgi_http_scope(self): "azure_functions.function_directory": self.af_function_directory, "azure_functions.function_name": self.af_function_name, "azure_functions.invocation_id": self.af_invocation_id, + "azure_functions.thread_local_storage": + self.af_thread_local_storage, "azure_functions.trace_context": self.af_trace_context, "azure_functions.retry_context": self.af_retry_context } diff --git a/azure/functions/_http_wsgi.py b/azure/functions/_http_wsgi.py index fdba6d95..6df5d163 100644 --- a/azure/functions/_http_wsgi.py +++ b/azure/functions/_http_wsgi.py @@ -56,6 +56,9 @@ def __init__(self, 'function_directory', None) self.af_function_name = getattr(func_ctx, 'function_name', None) self.af_invocation_id = getattr(func_ctx, 'invocation_id', None) + self.af_thread_local_storage = getattr(func_ctx, + 'thread_local_storage', + None) self.af_trace_context = getattr(func_ctx, 'trace_context', None) self.af_retry_context = getattr(func_ctx, 'retry_context', None) @@ -83,6 +86,8 @@ def to_environ(self, errors_buffer: StringIO) -> Dict[str, Any]: 'azure_functions.function_directory': self.af_function_directory, 'azure_functions.function_name': self.af_function_name, 'azure_functions.invocation_id': self.af_invocation_id, + 'azure_functions.thread_local_storage': + self.af_thread_local_storage, 'azure_functions.trace_context': self.af_trace_context, 'azure_functions.retry_context': self.af_retry_context } diff --git a/tests/test_http_asgi.py b/tests/test_http_asgi.py index 78a083b3..073817da 100644 --- a/tests/test_http_asgi.py +++ b/tests/test_http_asgi.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import asyncio +import threading import unittest import azure.functions as func @@ -111,14 +112,16 @@ def _generate_func_request( def _generate_func_context( self, invocation_id='123e4567-e89b-12d3-a456-426655440000', + thread_local_storage=threading.local(), function_name='httptrigger', function_directory='/home/roger/wwwroot/httptrigger', trace_context=TraceContext, retry_context=RetryContext ) -> func.Context: class MockContext(func.Context): - def __init__(self, ii, fn, fd, tc, rc): + def __init__(self, ii, tls, fn, fd, tc, rc): self._invocation_id = ii + self._thread_local_storage = tls self._function_name = fn self._function_directory = fd self._trace_context = tc @@ -128,6 +131,10 @@ def __init__(self, ii, fn, fd, tc, rc): def invocation_id(self): return self._invocation_id + @property + def thread_local_storage(self): + return self._thread_local_storage + @property def function_name(self): return self._function_name @@ -144,8 +151,8 @@ def trace_context(self): def retry_context(self): return self._retry_context - return MockContext(invocation_id, function_name, function_directory, - trace_context, retry_context) + return MockContext(invocation_id, thread_local_storage, function_name, + function_directory, trace_context, retry_context) def test_middleware_calls_app(self): app = MockAsgiApplication() diff --git a/tests/test_http_wsgi.py b/tests/test_http_wsgi.py index 701fc68e..6397a9d5 100644 --- a/tests/test_http_wsgi.py +++ b/tests/test_http_wsgi.py @@ -1,6 +1,6 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - +import threading import unittest from io import StringIO, BytesIO @@ -113,7 +113,8 @@ def test_request_parse_function_context(self): environ = WsgiRequest(func_request, func_context).to_environ(error_buffer) self.assertEqual(environ['azure_functions.invocation_id'], - '123e4567-e89b-12d3-a456-426655440000') + '123e4567-e89b-12d3-a456-426655440000'), + self.assertIsNotNone(environ['azure_functions.thread_local_storage']) self.assertEqual(environ['azure_functions.function_name'], 'httptrigger') self.assertEqual(environ['azure_functions.function_directory'], @@ -236,14 +237,16 @@ def _generate_func_request( def _generate_func_context( self, invocation_id='123e4567-e89b-12d3-a456-426655440000', + thread_local_storage=threading.local(), function_name='httptrigger', function_directory='/home/roger/wwwroot/httptrigger', trace_context=TraceContext, retry_context=RetryContext ) -> func.Context: class MockContext(func.Context): - def __init__(self, ii, fn, fd, tc, rc): + def __init__(self, ii, tls, fn, fd, tc, rc): self._invocation_id = ii + self._thread_local_storage = tls self._function_name = fn self._function_directory = fd self._trace_context = tc @@ -253,6 +256,10 @@ def __init__(self, ii, fn, fd, tc, rc): def invocation_id(self): return self._invocation_id + @property + def thread_local_storage(self): + return self._thread_local_storage + @property def function_name(self): return self._function_name @@ -269,7 +276,8 @@ def trace_context(self): def retry_context(self): return self._retry_context - return MockContext(invocation_id, function_name, function_directory, + return MockContext(invocation_id, thread_local_storage, + function_name, function_directory, trace_context, retry_context) def _generate_wsgi_app(self,