Skip to content

Commit

Permalink
Extension for initializing OpenCensus tracer into Azure Functions (#1010
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Hazhzeng authored Apr 28, 2021
1 parent 29fd633 commit 9bb688d
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
})
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions contrib/opencensus-ext-azure/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
113 changes: 113 additions & 0 deletions contrib/opencensus-ext-azure/tests/test_azure_functions_extension.py
Original file line number Diff line number Diff line change
@@ -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'))

0 comments on commit 9bb688d

Please sign in to comment.