diff --git a/runtimes/v1/azure_functions_runtime_v1/handle_event.py b/runtimes/v1/azure_functions_runtime_v1/handle_event.py index da3b39e2f..1a38bc329 100644 --- a/runtimes/v1/azure_functions_runtime_v1/handle_event.py +++ b/runtimes/v1/azure_functions_runtime_v1/handle_event.py @@ -11,7 +11,10 @@ from .functions import FunctionInfo, Registry from .loader import load_function, install from .logging import logger -from .otel import otel_manager, initialize_azure_monitor, configure_opentelemetry +from .otel import (configure_opentelemetry, + initialize_azure_monitor, + otel_manager, + update_opentelemetry_status) from .version import VERSION from .bindings.context import get_context @@ -70,6 +73,7 @@ async def worker_init_request(request): if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): otel_manager.set_otel_libs_available(True) + update_opentelemetry_status() if (otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available()): @@ -188,7 +192,8 @@ async def invocation_request(request): args[name] = Out() if fi.is_async: - if otel_manager.get_azure_monitor_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.get_otel_libs_available()): configure_opentelemetry(fi_context) # Not supporting Extensions @@ -291,6 +296,7 @@ async def function_environment_reload_request(request): capabilities = {} if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): otel_manager.set_otel_libs_available(True) + update_opentelemetry_status() if is_envvar_true(PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY): initialize_azure_monitor() diff --git a/runtimes/v1/azure_functions_runtime_v1/otel.py b/runtimes/v1/azure_functions_runtime_v1/otel.py index a73502c15..2e579a85d 100644 --- a/runtimes/v1/azure_functions_runtime_v1/otel.py +++ b/runtimes/v1/azure_functions_runtime_v1/otel.py @@ -107,10 +107,15 @@ def initialize_azure_monitor(): def configure_opentelemetry(invocation_context): + trace_context_propagator = otel_manager.get_trace_context_propagator() + context_api = otel_manager.get_context_api() + if trace_context_propagator is None or context_api is None: + return + carrier = {TRACEPARENT: invocation_context.trace_context.trace_parent, TRACESTATE: invocation_context.trace_context.trace_state} - ctx = otel_manager.get_trace_context_propagator().extract(carrier) - otel_manager.get_context_api().attach(ctx) + ctx = trace_context_propagator.extract(carrier) + context_api.attach(ctx) otel_manager = OTelManager() diff --git a/runtimes/v1/azure_functions_runtime_v1/utils/executor.py b/runtimes/v1/azure_functions_runtime_v1/utils/executor.py index 12dd04e40..0186355fb 100644 --- a/runtimes/v1/azure_functions_runtime_v1/utils/executor.py +++ b/runtimes/v1/azure_functions_runtime_v1/utils/executor.py @@ -31,7 +31,8 @@ def run_sync_func(invocation_id, context, func, params): context.thread_local_storage.invocation_id = invocation_id token = invocation_id_cv.set(invocation_id) try: - if otel_manager.get_azure_monitor_available(): + if (otel_manager.get_azure_monitor_available() + or otel_manager.get_otel_libs_available()): configure_opentelemetry(context) result = functools.partial(execute_sync, func) return result(params) diff --git a/runtimes/v1/tests/unittests/test_opentelemetry.py b/runtimes/v1/tests/unittests/test_opentelemetry.py index f992ac85d..48ad7f133 100644 --- a/runtimes/v1/tests/unittests/test_opentelemetry.py +++ b/runtimes/v1/tests/unittests/test_opentelemetry.py @@ -1,11 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. +import asyncio import os import unittest import tests.protos as protos from azure_functions_runtime_v1.handle_event import otel_manager, worker_init_request -from azure_functions_runtime_v1.otel import (initialize_azure_monitor, +from azure_functions_runtime_v1.otel import (configure_opentelemetry, + initialize_azure_monitor, update_opentelemetry_status) from azure_functions_runtime_v1.logging import logger from tests.utils.constants import UNIT_TESTS_FOLDER @@ -18,6 +20,24 @@ class TestOpenTelemetry(unittest.TestCase): + def setUp(self): + self._otel_manager_state = ( + otel_manager.get_azure_monitor_available(), + otel_manager.get_otel_libs_available(), + otel_manager.get_context_api(), + otel_manager.get_trace_context_propagator(), + ) + + def tearDown(self): + (azure_monitor_available, + otel_libs_available, + context_api, + trace_context_propagator) = self._otel_manager_state + otel_manager.set_azure_monitor_available(azure_monitor_available) + otel_manager.set_otel_libs_available(otel_libs_available) + otel_manager.set_context_api(context_api) + otel_manager.set_trace_context_propagator(trace_context_propagator) + def test_update_opentelemetry_status_import_error(self): with patch.dict('sys.modules', { 'opentelemetry': None, @@ -42,6 +62,12 @@ def test_update_opentelemetry_status_success( self.assertIsNotNone(otel_manager.get_context_api()) self.assertIsNotNone(otel_manager.get_trace_context_propagator()) + def test_configure_opentelemetry_noops_when_not_initialized(self): + otel_manager.set_context_api(None) + otel_manager.set_trace_context_propagator(None) + + configure_opentelemetry(MagicMock()) + @patch('builtins.__import__') @patch("azure_functions_runtime_v1.otel.update_opentelemetry_status") def test_initialize_azure_monitor_success( @@ -145,7 +171,7 @@ async def test_init_request_initialize_azure_monitor_disabled_app_setting( self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) @patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'true'}) - async def test_init_request_enable_opentelemetry_enabled_app_setting( + def test_init_request_enable_opentelemetry_enabled_app_setting( self, ): worker_request = WorkerRequest(name='worker_init_request', @@ -154,13 +180,15 @@ async def test_init_request_enable_opentelemetry_enabled_app_setting( FUNCTION_APP_DIRECTORY)), properties={'host': '123', 'protos': protos}) - init_response = await worker_init_request(worker_request) + init_response = asyncio.run(worker_init_request(worker_request)) self.assertEqual(init_response.result.status, protos.StatusResult.Success) - # Verify otel_libs_available is set to True - self.assertTrue(otel_manager.get_azure_monitor_available()) + # Verify OpenTelemetry propagation is initialized + self.assertTrue(otel_manager.get_otel_libs_available()) + self.assertIsNotNone(otel_manager.get_context_api()) + self.assertIsNotNone(otel_manager.get_trace_context_propagator()) # Verify that WorkerOpenTelemetryEnabled capability is set to _TRUE capabilities = init_response.capabilities self.assertIn("WorkerOpenTelemetryEnabled", capabilities) diff --git a/runtimes/v2/azure_functions_runtime/handle_event.py b/runtimes/v2/azure_functions_runtime/handle_event.py index 0ed133f93..68dde31be 100644 --- a/runtimes/v2/azure_functions_runtime/handle_event.py +++ b/runtimes/v2/azure_functions_runtime/handle_event.py @@ -18,7 +18,10 @@ ) from .loader import index_function_app, process_indexed_function from .logging import logger -from .otel import otel_manager, configure_opentelemetry, initialize_azure_monitor +from .otel import (configure_opentelemetry, + initialize_azure_monitor, + otel_manager, + update_opentelemetry_status) from .version import VERSION from .bindings.context import get_context @@ -86,6 +89,7 @@ async def worker_init_request(request): if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): otel_manager.set_otel_libs_available(True) + update_opentelemetry_status() if (otel_manager.get_azure_monitor_available() or otel_manager.get_otel_libs_available()): @@ -337,6 +341,7 @@ async def function_environment_reload_request(request): capabilities = {} if is_envvar_true(PYTHON_ENABLE_OPENTELEMETRY): otel_manager.set_otel_libs_available(True) + update_opentelemetry_status() if is_envvar_true(PYTHON_APPLICATIONINSIGHTS_ENABLE_TELEMETRY): initialize_azure_monitor() diff --git a/runtimes/v2/azure_functions_runtime/otel.py b/runtimes/v2/azure_functions_runtime/otel.py index 3f608fa50..c8b7ecb79 100644 --- a/runtimes/v2/azure_functions_runtime/otel.py +++ b/runtimes/v2/azure_functions_runtime/otel.py @@ -107,10 +107,15 @@ def initialize_azure_monitor(): def configure_opentelemetry(invocation_context): + trace_context_propagator = otel_manager.get_trace_context_propagator() + context_api = otel_manager.get_context_api() + if trace_context_propagator is None or context_api is None: + return + carrier = {TRACEPARENT: invocation_context.trace_context.trace_parent, TRACESTATE: invocation_context.trace_context.trace_state} - ctx = otel_manager.get_trace_context_propagator().extract(carrier) - otel_manager.get_context_api().attach(ctx) + ctx = trace_context_propagator.extract(carrier) + context_api.attach(ctx) otel_manager = OTelManager() diff --git a/runtimes/v2/tests/unittests/test_opentelemetry.py b/runtimes/v2/tests/unittests/test_opentelemetry.py index 696dfcb55..53fc29adf 100644 --- a/runtimes/v2/tests/unittests/test_opentelemetry.py +++ b/runtimes/v2/tests/unittests/test_opentelemetry.py @@ -1,11 +1,13 @@ # Copyright (c) Microsoft Corporation. All rights reserved. +import asyncio import os import unittest import tests.protos as protos from azure_functions_runtime.handle_event import otel_manager, worker_init_request -from azure_functions_runtime.otel import (initialize_azure_monitor, +from azure_functions_runtime.otel import (configure_opentelemetry, + initialize_azure_monitor, update_opentelemetry_status, OTelManager) from azure_functions_runtime.logging import logger @@ -19,6 +21,24 @@ class TestOpenTelemetry(unittest.TestCase): + def setUp(self): + self._otel_manager_state = ( + otel_manager.get_azure_monitor_available(), + otel_manager.get_otel_libs_available(), + otel_manager.get_context_api(), + otel_manager.get_trace_context_propagator(), + ) + + def tearDown(self): + (azure_monitor_available, + otel_libs_available, + context_api, + trace_context_propagator) = self._otel_manager_state + otel_manager.set_azure_monitor_available(azure_monitor_available) + otel_manager.set_otel_libs_available(otel_libs_available) + otel_manager.set_context_api(context_api) + otel_manager.set_trace_context_propagator(trace_context_propagator) + def test_update_opentelemetry_status_import_error(self): with patch.dict('sys.modules', { 'opentelemetry': None, @@ -43,6 +63,12 @@ def test_update_opentelemetry_status_success( self.assertIsNotNone(otel_manager.get_context_api()) self.assertIsNotNone(otel_manager.get_trace_context_propagator()) + def test_configure_opentelemetry_noops_when_not_initialized(self): + otel_manager.set_context_api(None) + otel_manager.set_trace_context_propagator(None) + + configure_opentelemetry(MagicMock()) + @patch('builtins.__import__') @patch("azure_functions_runtime.otel.update_opentelemetry_status") def test_initialize_azure_monitor_success( @@ -146,7 +172,7 @@ async def test_init_request_initialize_azure_monitor_disabled_app_setting( self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities) @patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'true'}) - async def test_init_request_enable_opentelemetry_enabled_app_setting( + def test_init_request_enable_opentelemetry_enabled_app_setting( self, ): worker_request = WorkerRequest(name='worker_init_request', @@ -155,13 +181,15 @@ async def test_init_request_enable_opentelemetry_enabled_app_setting( FUNCTION_APP_DIRECTORY)), properties={'host': '123', 'protos': protos}) - init_response = await worker_init_request(worker_request) + init_response = asyncio.run(worker_init_request(worker_request)) self.assertEqual(init_response.result.status, protos.StatusResult.Success) - # Verify otel_libs_available is set to True - self.assertTrue(otel_manager.get_azure_monitor_available()) + # Verify OpenTelemetry propagation is initialized + self.assertTrue(otel_manager.get_otel_libs_available()) + self.assertIsNotNone(otel_manager.get_context_api()) + self.assertIsNotNone(otel_manager.get_trace_context_propagator()) # Verify that WorkerOpenTelemetryEnabled capability is set to _TRUE capabilities = init_response.capabilities self.assertIn("WorkerOpenTelemetryEnabled", capabilities)