diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b267b708..8bb678ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - Updated `azure` module ([#886](https://github.com/census-instrumentation/opencensus-python/pull/886)) +- Updated `azure` module to enable Azure Functions integration +([#1010](https://github.com/census-instrumentation/opencensus-python/pull/1010)) - PeriodicMetricTask flush on exit ([#943](https://github.com/census-instrumentation/opencensus-python/pull/943)) - Change blacklist to excludelist diff --git a/contrib/opencensus-ext-azure/examples/traces/azure_functions_extension.py b/contrib/opencensus-ext-azure/examples/traces/azure_functions_extension.py new file mode 100644 index 000000000..3b319c796 --- /dev/null +++ b/contrib/opencensus-ext-azure/examples/traces/azure_functions_extension.py @@ -0,0 +1,46 @@ +# Copyright 2021, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: Configure PYTHON_ENABLE_WORKER_EXTENSIONS = 1 function app setting. +# Ensure opencensus-ext-azure, opencensus-ext-requests and azure-functions +# are defined in your function app's requirements.txt and properly installed. +# +# For more information about getting started with Azure Functions, please visit +# https://aka.ms/functions-python-vscode +import json +import logging + +import requests + +from opencensus.ext.azure.extension.azure_functions import OpenCensusExtension + +OpenCensusExtension.configure( + libraries=['requests'], + connection_string='InstrumentationKey=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +) + +def main(req, context): + logging.info('Executing HttpTrigger with OpenCensus extension') + + with context.tracer.span("parent"): + requests.get(url='http://example.com') + + return json.dumps({ + 'method': req.method, + 'ctx_func_name': context.function_name, + 'ctx_func_dir': context.function_directory, + 'ctx_invocation_id': context.invocation_id, + 'ctx_trace_context_Traceparent': context.trace_context.Traceparent, + 'ctx_trace_context_Tracestate': context.trace_context.Tracestate, + }) diff --git a/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/__init__.py b/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/__init__.py new file mode 100644 index 000000000..8e6fabe61 --- /dev/null +++ b/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2021, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/azure_functions.py b/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/azure_functions.py new file mode 100644 index 000000000..96e0c9e9f --- /dev/null +++ b/contrib/opencensus-ext-azure/opencensus/ext/azure/extension/azure_functions.py @@ -0,0 +1,106 @@ +# Copyright 2021, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from azure.functions import AppExtensionBase +from opencensus.trace import config_integration +from opencensus.trace.propagation.trace_context_http_header_format import ( + TraceContextPropagator, +) +from opencensus.trace.samplers import ProbabilitySampler +from opencensus.trace.tracer import Tracer + +from ..trace_exporter import AzureExporter + + +class OpenCensusExtension(AppExtensionBase): + """Extension for Azure Functions integration to export traces into Azure + Monitor. Ensure the following requirements are met: + 1. Azure Functions version is greater or equal to v3.0.15584 + 2. App setting PYTHON_ENABLE_WORKER_EXTENSIONS is set to 1 + """ + + @classmethod + def init(cls): + cls._exporter = None + cls._trace_integrations = [] + + @classmethod + def configure(cls, + libraries, + connection_string = None, + *args, + **kwargs): + """Configure libraries for integrating into OpenCensus extension. + Initialize an Azure Exporter that will write traces to AppInsights. + :type libraries: List[str] + :param libraries: the libraries opencensus-ext-* that need to be + integrated into OpenCensus tracer. (e.g. ['requests']) + :type connection_string: Optional[str] + :param connection_string: the connection string of azure exporter + to write into. If this is set to None, the extension will use + an instrumentation connection string from your app settings. + """ + cls._trace_integrations = config_integration.trace_integrations( + libraries + ) + + cls._exporter = AzureExporter(connection_string=connection_string) + + @classmethod + def pre_invocation_app_level(cls, + logger, + context, + func_args = {}, + *args, + **kwargs): + """An implementation of pre invocation hooks on Function App's level. + The Python Worker Extension Interface is defined in + https://github.com/Azure/azure-functions-python-library/ + blob/dev/azure/functions/extension/app_extension_base.py + """ + if not cls._exporter: + logger.warning( + 'Please call OpenCensusExtension.configure() after the import ' + 'statement to ensure AzureExporter is setup correctly.' + ) + return + + span_context = TraceContextPropagator().from_headers({ + "traceparent": context.trace_context.Traceparent, + "tracestate": context.trace_context.Tracestate + }) + + tracer = Tracer( + span_context=span_context, + exporter=cls._exporter, + sampler=ProbabilitySampler(1.0) + ) + + setattr(context, 'tracer', tracer) + + @classmethod + def post_invocation_app_level(cls, + logger, + context, + func_args, + func_ret, + *args, + **kwargs): + """An implementation of post invocation hooks on Function App's level. + The Python Worker Extension Interface is defined in + https://github.com/Azure/azure-functions-python-library/ + blob/dev/azure/functions/extension/app_extension_base.py + """ + if getattr(context, 'tracer', None): + del context.tracer diff --git a/contrib/opencensus-ext-azure/setup.py b/contrib/opencensus-ext-azure/setup.py index d4f228478..3af870ba2 100644 --- a/contrib/opencensus-ext-azure/setup.py +++ b/contrib/opencensus-ext-azure/setup.py @@ -39,6 +39,7 @@ include_package_data=True, long_description=open('README.rst').read(), install_requires=[ + 'azure-functions >= 1.7.0', 'opencensus >= 0.8.dev0, < 1.0.0', 'psutil >= 5.6.3', 'requests >= 2.19.0', diff --git a/contrib/opencensus-ext-azure/tests/test_azure_functions_extension.py b/contrib/opencensus-ext-azure/tests/test_azure_functions_extension.py new file mode 100644 index 000000000..81fa6470f --- /dev/null +++ b/contrib/opencensus-ext-azure/tests/test_azure_functions_extension.py @@ -0,0 +1,113 @@ +# Copyright 2021, OpenCensus Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import os +import sys +import unittest + +import mock + +from opencensus.ext.azure.extension.azure_functions import OpenCensusExtension + +IS_SUPPORTED_PYTHON_VERSION = sys.version_info.major == 3 + +MOCK_APPINSIGHTS_KEY = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' +MOCK_AZURE_EXPORTER_CONNSTRING = ( + 'InstrumentationKey=11111111-2222-3333-4444-555555555555;' + 'IngestionEndpoint=https://mock.in.applicationinsights.azure.com/' +) + +unittest.skipIf( + not IS_SUPPORTED_PYTHON_VERSION, + 'Azure Functions only support Python 3.x' +) +class MockContext(object): + class MockTraceContext(object): + Tracestate = 'rojo=00f067aa0ba902b7' + Traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-5fd358d59f88ce45-01' + + trace_context = MockTraceContext() + +class TestAzureFunctionsExtension(unittest.TestCase): + def setUp(self): + self._instance = OpenCensusExtension + OpenCensusExtension.init() + os.environ['APPINSIGHTS_INSTRUMENTATIONKEY'] = MOCK_APPINSIGHTS_KEY + + def tearDown(self): + if 'APPINSIGHTS_INSTRUMENTATIONKEY' in os.environ: + del os.environ['APPINSIGHTS_INSTRUMENTATIONKEY'] + + @mock.patch('opencensus.ext.azure.extension.azure_functions' + '.config_integration') + def test_configure_method_should_setup_trace_integration(self, cfg_mock): + self._instance.configure(['requests']) + cfg_mock.trace_integrations.assert_called_once_with(['requests']) + + @mock.patch('opencensus.ext.azure.extension.azure_functions' + '.AzureExporter') + def test_configure_method_should_setup_azure_exporter( + self, + azure_exporter_mock + ): + self._instance.configure(['requests']) + azure_exporter_mock.assert_called_with(connection_string=None) + + @mock.patch('opencensus.ext.azure.extension.azure_functions' + '.AzureExporter') + def test_configure_method_shouold_setup_azure_exporter_with_connstring( + self, + azure_exporter_mock + ): + self._instance.configure(['request'], MOCK_AZURE_EXPORTER_CONNSTRING) + azure_exporter_mock.assert_called_with( + connection_string=MOCK_AZURE_EXPORTER_CONNSTRING + ) + + def test_pre_invocation_should_warn_if_not_configured(self): + mock_context = MockContext() + mock_logger = mock.Mock() + self._instance.pre_invocation_app_level(mock_logger, mock_context) + mock_logger.warning.assert_called_once() + + def test_pre_invocation_should_attach_tracer_to_context(self): + # Attach a mock object to exporter + self._instance._exporter = mock.Mock() + + # Check if the tracer is attached to mock_context + mock_context = MockContext() + mock_logger = mock.Mock() + self._instance.pre_invocation_app_level(mock_logger, mock_context) + self.assertTrue(hasattr(mock_context, 'tracer')) + + def test_post_invocation_should_ignore_tracer_deallocation_if_not_set(self): + mock_context = MockContext() + mock_logger = mock.Mock() + mock_func_args = {} + mock_func_ret = None + self._instance.post_invocation_app_level( + mock_logger, mock_context, mock_func_args, mock_func_ret + ) + + def test_post_invocation_should_delete_tracer_from_context(self): + mock_context = MockContext() + mock_tracer = mock.Mock() + setattr(mock_context, 'tracer', mock_tracer) + mock_logger = mock.Mock() + mock_func_args = {} + mock_func_ret = None + self._instance.post_invocation_app_level( + mock_logger, mock_context, mock_func_args, mock_func_ret + ) + self.assertFalse(hasattr(mock_context, 'tracer'))