Skip to content

Commit d5c02fb

Browse files
lzchenhallvictoriagavin-aguiarVictoria Hall
authored
feat: Support OpenTelemetry Azure monitor distro (#1509)
* distro-commit * resource detector * tests * lint * Update constants.py * fix * Update test_opentelemetry.py * Update test_opentelemetry.py * fixing tests, added setting to logs * reordered import statements * formatting * rename * fix mock env variable * Update dispatcher.py * Update dispatcher.py --------- Co-authored-by: hallvictoria <[email protected]> Co-authored-by: gavin-aguiar <[email protected]> Co-authored-by: Victoria Hall <[email protected]>
1 parent 2f8f424 commit d5c02fb

File tree

4 files changed

+108
-19
lines changed

4 files changed

+108
-19
lines changed

azure_functions_worker/constants.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -81,5 +81,15 @@
8181
# Base extension supported Python minor version
8282
BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8
8383

84+
# Appsetting to turn on OpenTelemetry support/features
85+
# Includes turning on Azure monitor distro to send telemetry to AppInsights
8486
PYTHON_ENABLE_OPENTELEMETRY = "PYTHON_ENABLE_OPENTELEMETRY"
85-
PYTHON_ENABLE_OPENTELEMETRY_DEFAULT = True
87+
PYTHON_ENABLE_OPENTELEMETRY_DEFAULT = False
88+
89+
# Appsetting to specify root logger name of logger to collect telemetry for
90+
# Used by Azure monitor distro
91+
PYTHON_AZURE_MONITOR_LOGGER_NAME = "PYTHON_AZURE_MONITOR_LOGGER_NAME"
92+
PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT = ""
93+
94+
# Appsetting to specify AppInsights connection string
95+
APPLICATIONINSIGHTS_CONNECTION_STRING = "APPLICATIONINSIGHTS_CONNECTION_STRING"

azure_functions_worker/dispatcher.py

+53-12
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@
2323
from . import bindings, constants, functions, loader, protos
2424
from .bindings.shared_memory_data_transfer import SharedMemoryManager
2525
from .constants import (
26+
APPLICATIONINSIGHTS_CONNECTION_STRING,
2627
METADATA_PROPERTIES_WORKER_INDEXED,
28+
PYTHON_AZURE_MONITOR_LOGGER_NAME,
29+
PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT,
2730
PYTHON_ENABLE_DEBUG_LOGGING,
2831
PYTHON_ENABLE_INIT_INDEXING,
2932
PYTHON_ENABLE_OPENTELEMETRY,
@@ -99,7 +102,7 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
99102
self._function_metadata_exception = None
100103

101104
# Used for checking if open telemetry is enabled
102-
self._otel_libs_available = False
105+
self._azure_monitor_available = False
103106
self._context_api = None
104107
self._trace_context_propagator = None
105108

@@ -288,6 +291,46 @@ async def _dispatch_grpc_request(self, request):
288291
resp = await request_handler(request)
289292
self._grpc_resp_queue.put_nowait(resp)
290293

294+
def initialize_azure_monitor(self):
295+
"""Initializes OpenTelemetry and Azure monitor distro
296+
"""
297+
self.update_opentelemetry_status()
298+
try:
299+
from azure.monitor.opentelemetry import configure_azure_monitor
300+
301+
# Set functions resource detector manually until officially
302+
# include in Azure monitor distro
303+
os.environ.setdefault(
304+
"OTEL_EXPERIMENTAL_RESOURCE_DETECTORS",
305+
"azure_functions",
306+
)
307+
308+
configure_azure_monitor(
309+
# Connection string can be explicitly specified in Appsetting
310+
# If not set, defaults to env var
311+
# APPLICATIONINSIGHTS_CONNECTION_STRING
312+
connection_string=get_app_setting(
313+
setting=APPLICATIONINSIGHTS_CONNECTION_STRING
314+
),
315+
logger_name=get_app_setting(
316+
setting=PYTHON_AZURE_MONITOR_LOGGER_NAME,
317+
default_value=PYTHON_AZURE_MONITOR_LOGGER_NAME_DEFAULT
318+
),
319+
)
320+
self._azure_monitor_available = True
321+
322+
logger.info("Successfully configured Azure monitor distro.")
323+
except ImportError:
324+
logger.exception(
325+
"Cannot import Azure Monitor distro."
326+
)
327+
self._azure_monitor_available = False
328+
except Exception:
329+
logger.exception(
330+
"Error initializing Azure monitor distro."
331+
)
332+
self._azure_monitor_available = False
333+
291334
def update_opentelemetry_status(self):
292335
"""Check for OpenTelemetry library availability and
293336
update the status attribute."""
@@ -299,12 +342,11 @@ def update_opentelemetry_status(self):
299342

300343
self._context_api = context_api
301344
self._trace_context_propagator = TraceContextTextMapPropagator()
302-
self._otel_libs_available = True
303345

304-
logger.info("Successfully loaded OpenTelemetry modules. "
305-
"OpenTelemetry is now enabled.")
306346
except ImportError:
307-
self._otel_libs_available = False
347+
logger.exception(
348+
"Cannot import OpenTelemetry libraries."
349+
)
308350

309351
async def _handle__worker_init_request(self, request):
310352
logger.info('Received WorkerInitRequest, '
@@ -334,12 +376,11 @@ async def _handle__worker_init_request(self, request):
334376
constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE,
335377
constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
336378
}
337-
338379
if get_app_setting(setting=PYTHON_ENABLE_OPENTELEMETRY,
339380
default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT):
340-
self.update_opentelemetry_status()
381+
self.initialize_azure_monitor()
341382

342-
if self._otel_libs_available:
383+
if self._azure_monitor_available:
343384
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = _TRUE
344385

345386
if DependencyManager.should_load_cx_dependencies():
@@ -611,7 +652,7 @@ async def _handle__invocation_request(self, request):
611652
args[name] = bindings.Out()
612653

613654
if fi.is_async:
614-
if self._otel_libs_available:
655+
if self._azure_monitor_available:
615656
self.configure_opentelemetry(fi_context)
616657

617658
call_result = \
@@ -731,9 +772,9 @@ async def _handle__function_environment_reload_request(self, request):
731772
if get_app_setting(
732773
setting=PYTHON_ENABLE_OPENTELEMETRY,
733774
default_value=PYTHON_ENABLE_OPENTELEMETRY_DEFAULT):
734-
self.update_opentelemetry_status()
775+
self.initialize_azure_monitor()
735776

736-
if self._otel_libs_available:
777+
if self._azure_monitor_available:
737778
capabilities[constants.WORKER_OPEN_TELEMETRY_ENABLED] = (
738779
_TRUE)
739780

@@ -944,7 +985,7 @@ def _run_sync_func(self, invocation_id, context, func, params):
944985
# invocation_id from ThreadPoolExecutor's threads.
945986
context.thread_local_storage.invocation_id = invocation_id
946987
try:
947-
if self._otel_libs_available:
988+
if self._azure_monitor_available:
948989
self.configure_opentelemetry(context)
949990
return ExtensionManager.get_sync_invocation_wrapper(context,
950991
func)(params)

azure_functions_worker/utils/app_setting_manager.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED,
88
PYTHON_ENABLE_DEBUG_LOGGING,
99
PYTHON_ENABLE_INIT_INDEXING,
10+
PYTHON_ENABLE_OPENTELEMETRY,
1011
PYTHON_ENABLE_WORKER_EXTENSIONS,
1112
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT,
1213
PYTHON_ENABLE_WORKER_EXTENSIONS_DEFAULT_39,
@@ -27,7 +28,8 @@ def get_python_appsetting_state():
2728
PYTHON_ENABLE_WORKER_EXTENSIONS,
2829
FUNCTIONS_WORKER_SHARED_MEMORY_DATA_TRANSFER_ENABLED,
2930
PYTHON_SCRIPT_FILE_NAME,
30-
PYTHON_ENABLE_INIT_INDEXING]
31+
PYTHON_ENABLE_INIT_INDEXING,
32+
PYTHON_ENABLE_OPENTELEMETRY]
3133

3234
app_setting_states = "".join(
3335
f"{app_setting}: {current_vars[app_setting]} | "

tests/unittests/test_opentelemetry.py

+41-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import os
23
import unittest
34
from unittest.mock import MagicMock, patch
45

@@ -23,18 +24,45 @@ def test_update_opentelemetry_status_import_error(self):
2324
with patch('builtins.__import__', side_effect=ImportError):
2425
self.dispatcher.update_opentelemetry_status()
2526
# Verify that otel_libs_available is set to False due to ImportError
26-
self.assertFalse(self.dispatcher._otel_libs_available)
27+
self.assertFalse(self.dispatcher._azure_monitor_available)
2728

2829
@patch('builtins.__import__')
2930
def test_update_opentelemetry_status_success(
3031
self, mock_imports):
3132
mock_imports.return_value = MagicMock()
3233
self.dispatcher.update_opentelemetry_status()
33-
self.assertTrue(self.dispatcher._otel_libs_available)
34+
self.assertIsNotNone(self.dispatcher._context_api)
35+
self.assertIsNotNone(self.dispatcher._trace_context_propagator)
3436

3537
@patch('builtins.__import__')
36-
def test_init_request_otel_capability_enabled(
37-
self, mock_imports):
38+
@patch("azure_functions_worker.dispatcher.Dispatcher.update_opentelemetry_status")
39+
def test_initialize_azure_monitor_success(
40+
self,
41+
mock_update_ot,
42+
mock_imports,
43+
):
44+
mock_imports.return_value = MagicMock()
45+
self.dispatcher.initialize_azure_monitor()
46+
mock_update_ot.assert_called_once()
47+
self.assertTrue(self.dispatcher._azure_monitor_available)
48+
49+
@patch("azure_functions_worker.dispatcher.Dispatcher.update_opentelemetry_status")
50+
def test_initialize_azure_monitor_import_error(
51+
self,
52+
mock_update_ot,
53+
):
54+
with patch('builtins.__import__', side_effect=ImportError):
55+
self.dispatcher.initialize_azure_monitor()
56+
mock_update_ot.assert_called_once()
57+
# Verify that otel_libs_available is set to False due to ImportError
58+
self.assertFalse(self.dispatcher._azure_monitor_available)
59+
60+
@patch.dict(os.environ, {'PYTHON_ENABLE_OPENTELEMETRY': 'true'})
61+
@patch('builtins.__import__')
62+
def test_init_request_otel_capability_enabled_app_setting(
63+
self,
64+
mock_imports,
65+
):
3866
mock_imports.return_value = MagicMock()
3967

4068
init_request = protos.StreamingMessage(
@@ -55,7 +83,11 @@ def test_init_request_otel_capability_enabled(
5583
self.assertIn("WorkerOpenTelemetryEnabled", capabilities)
5684
self.assertEqual(capabilities["WorkerOpenTelemetryEnabled"], "true")
5785

58-
def test_init_request_otel_capability_disabled(self):
86+
@patch("azure_functions_worker.dispatcher.Dispatcher.initialize_azure_monitor")
87+
def test_init_request_otel_capability_disabled_app_setting(
88+
self,
89+
mock_initialize_azmon,
90+
):
5991

6092
init_request = protos.StreamingMessage(
6193
worker_init_request=protos.WorkerInitRequest(
@@ -70,5 +102,9 @@ def test_init_request_otel_capability_disabled(self):
70102
self.assertEqual(init_response.worker_init_response.result.status,
71103
protos.StatusResult.Success)
72104

105+
# Azure monitor initialized not called
106+
mock_initialize_azmon.assert_not_called()
107+
108+
# Verify that WorkerOpenTelemetryEnabled capability is not set
73109
capabilities = init_response.worker_init_response.capabilities
74110
self.assertNotIn("WorkerOpenTelemetryEnabled", capabilities)

0 commit comments

Comments
 (0)