├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── azure_monitor ├── README.md ├── artifacts │ ├── common.ps1 │ ├── parameters.json │ ├── setup.ps1 │ └── template.json ├── azfunc_sample │ ├── BaltimoreCyberTrustRoot.crt.pem │ ├── DigiCertGlobalRootCA.crt.pem │ ├── MyHttpFunction │ │ ├── __init__.py │ │ ├── function.json │ │ └── sample.dat │ ├── README.md │ ├── docs │ │ └── assets │ │ │ ├── 01-ai-correlated-invocation.PNG │ │ │ ├── 02-ai-correlated-invocation.PNG │ │ │ └── 03-ai-application-map.PNG │ ├── host.json │ ├── instrumentation │ │ ├── __init__.py │ │ ├── function.json │ │ ├── globals.py │ │ ├── instrumentation_func.py │ │ ├── main.py │ │ └── utils.py │ ├── local.settings.json │ ├── requirements.txt │ ├── tests │ │ ├── __init__.py │ │ ├── test_globals.py │ │ └── test_instrumentation_func.py │ └── tmp │ │ └── data │ │ ├── 05582fa0-2f83-48ca-96ae-8206ee59aab5.txt │ │ ├── a20f2b98-bd5a-4692-ab93-397a460d722f.txt │ │ └── bd7ae612-ca6a-4bb4-a146-9926f24a4e10.txt ├── django_sample │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── azureproject │ │ ├── __init__.py │ │ ├── app_insights.py │ │ ├── asgi.py │ │ ├── development.py │ │ ├── get_token.py │ │ ├── production.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── manage.py │ ├── requirements.txt │ ├── restaurant_review │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_alter_review_rating.py │ │ │ ├── 0003_restaurant_image_name_review_image_name.py │ │ │ ├── 0004_alter_restaurant_image_name_alter_review_image_name.py │ │ │ ├── 0005_remove_restaurant_image_name.py │ │ │ ├── 0006_alter_review_image_name.py │ │ │ ├── 0007_alter_review_image_name.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templates │ │ │ ├── 500.html │ │ │ └── restaurant_review │ │ │ │ ├── base.html │ │ │ │ ├── create_restaurant.html │ │ │ │ ├── details.html │ │ │ │ ├── index.html │ │ │ │ └── star_rating.html │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── restaurant_extras.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── static │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── Example-reviews.png │ │ │ └── azure-icon.svg │ │ └── restaurant.css │ ├── staticfiles │ │ └── note.txt │ └── web_project │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py ├── env.template ├── flask_sample │ ├── README.md │ ├── app.db │ ├── app │ │ ├── __init__.py │ │ ├── forms │ │ │ └── __init__.py │ │ ├── metrics.py │ │ ├── models │ │ │ └── __init__.py │ │ ├── routes.py │ │ ├── static │ │ │ └── index.css │ │ └── templates │ │ │ ├── blacklist.html │ │ │ └── index.html │ ├── command.py │ ├── config.py │ ├── endpoint.py │ ├── endpoint │ │ ├── __init__.py │ │ └── endpoint_routes.py │ ├── output │ │ └── file.txt │ ├── requirements.txt │ └── sample.py ├── media │ ├── python_azure_dependencies.png │ ├── python_azure_resources.png │ ├── python_custommetrics-carrots-logs.png │ ├── python_custommetrics-carrots.png │ ├── python_custommetrics-web-reviews.png │ ├── python_functionapp_deploy.png │ ├── python_simple_app_trace.png │ ├── python_simple_exception_custom.png │ ├── python_simple_trace_trace.png │ └── python_webapp_requests.png ├── python_logger_opencensus_azure │ ├── README.md │ └── monitoring │ │ ├── examples │ │ ├── README.md │ │ ├── __init__.py │ │ ├── api_1.py │ │ ├── api_2.py │ │ ├── api_3.py │ │ ├── api_4.py │ │ ├── client.py │ │ ├── logging_config.json │ │ └── util.py │ │ ├── img │ │ ├── application_map.png │ │ ├── dependency.png │ │ └── operation_id.png │ │ ├── requirements.txt │ │ ├── src │ │ ├── __init__.py │ │ └── logger.py │ │ └── tests │ │ ├── __init__.py │ │ └── test_logger.py └── simple_sample │ ├── customDimensions.py │ ├── database.py │ ├── event.py │ ├── metric.py │ ├── metric2.py │ ├── module1.py │ ├── module2.py │ ├── prompt.py │ ├── readme.md │ ├── spanComplex.py │ ├── spanSimple.py │ └── trace.py ├── setup.cfg ├── setup.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VSCode 132 | .vscode/ 133 | 134 | # Azurite 135 | __azurite_* 136 | __blobstorage__/ 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - python 5 | - html 6 | products: 7 | - azure 8 | description: "This repo contains sample applications to show how you can instrument the OpenCensus Azure Monitor exporters as well as track telemetry from popular Python libraries via OpenCensus integrations." 9 | urlFragment: azure-monitor-opencensus-python 10 | --- 11 | 12 | # Azure Monitor Python Sample Applications 13 | 14 | This repo contains following sample applications. 15 | 16 | ## Flask "To-Do" Sample Application 17 | 18 | Here is the [README](./azure_monitor/flask_sample/README.md) for the Flask "To-Do" Sample application. 19 | 20 | ## Python Logger class using Opencensus 21 | 22 | Here is the [README](./azure_monitor/python_logger_opencensus_azure/README.md) for the Python logger class Sample application. 23 | 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets Microsoft's [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)) of a security vulnerability, please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /azure_monitor/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started with App Insights for Python via OpenCensus Samples' 3 | description: Getting Started with App Insights for Python via OpenCensus Samples. 4 | author: givenscj 5 | ms.author: givenscj 6 | ms.devlang: python 7 | ms.topic: sample 8 | ms.date: 07/27/2022 9 | ms.custom: [] 10 | --- 11 | 12 | # Getting Started with App Insights for Python via OpenCensus Samples 13 | 14 | Many of the samples in this folder support the https://docs.microsoft.com/azure/azure-monitor/app/opencensus-python articles. Reference these articles for more information on how OpenCensus works with Python and Application Insights. 15 | 16 | The following samples are available in this repo: 17 | 18 | - [Azure Functions - Utilizes dependency tracking via MySQL and Azure Storage](./azfunc_sample/README.md) 19 | - [Django - Utilizes custom metrics based on web application events in Azure App Service](./django_sample/README.md) 20 | - [Flask - Utilizes OpenCensus Request and SqlAlchemy dependency tracking](./flask_sample/README.md) 21 | - [Python (Simple) - Simple python applications that demonstrate OpenCensus distributed tracing, metrics and logging](./simple_sample/README.md) 22 | - [Python (Logger) - Python applications that utilize OpenCensus tracing and dependencies](./python_logger_opencensus_azure/README.md) 23 | 24 | You can also find more samples in the following repos and documentation links however you may find that you have to setup more items manually that what is provided for you in the above samples: 25 | 26 | - [Quickstart: Deploy a Python (Django or Flask) web app to Azure App Service](https://docs.microsoft.com/azure/app-service/quickstart-python) 27 | - [Deploy a Python (Django or Flask) web app with PostgreSQL in Azure](https://docs.microsoft.com/azure/app-service/tutorial-python-postgresql-app) 28 | 29 | ## Setup 30 | 31 | This procedure configures your Python environment to send telemetry to the Application Insights feature of the Azure Monitor service with the OpenCensus Azure monitor exporters. It works with any Python application on-premises or in the cloud. 32 | 33 | ## Prerequisites 34 | 35 | To get started with the following samples, you need to: 36 | 37 | - Install Python (all of the samples have been tested to work with version 3.9.x and the Azure resources are deployed using 3.9). 38 | - Install the latest version of Visual Studio Code with the following extensions. 39 | - [ms-python.python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) 40 | - [ms-azuretools.vscode-azurefunctions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) 41 | - Install [Azure Functions Core Tools](https://docs.microsoft.com/azure/azure-functions/functions-run-local?tabs=v4%2Cwindows%2Ccsharp%2Cportal%2Cbash#install-the-azure-functions-core-tools) 42 | - Create a [free Azure account](https://azure.microsoft.com/free/) if you don't already have an Azure subscription. 43 | - Install [Azure Storage Emulator](https://docs.microsoft.com/azure/storage/common/storage-use-emulator) (for local Function App testing) 44 | 45 | ## Deploy the environment 46 | 47 | - Open a Windows PowerShell window. 48 | - Run the following: 49 | 50 | ```Powershell 51 | cd $home; 52 | 53 | rm -r python-appinsights -f 54 | 55 | git clone https://github.com/Azure-Samples/azure-monitor-opencensus-python python-appinsights 56 | ``` 57 | 58 | - Open Visual Studio Code to the cloned repositiory. 59 | - Open a new Windows PowerShell window, run the following: 60 | 61 | ```powershell 62 | cd azure_monitor/artifacts 63 | 64 | setup.ps1 65 | ``` 66 | 67 | - This will deploy the ARM template that contains all the necesary resources needed to run these samples. 68 | - Reference the `template.json` file for the ARM template that is deployed. It contains the following resources: 69 | 70 | ![The resources that are deployed via ARM Template.](../media/../azure_monitor/media/python_azure_resources.png "Screenshot of all the deployed azure resources.") 71 | 72 | - It will also update the environment files for the sample files with the specific connection string details from the deployment to speed up your sample exploration. 73 | - Additionally it will download sample files from the [OpenCensus Python](https://github.com/census-instrumentation/opencensus-python) repo. 74 | 75 | ## Configure the environment (Azure) 76 | 77 | - In the Azure Portal, browse to the `python-insights-SUFFIX` Azure Database for SQL. 78 | - Under **Security**, select **Networking**. 79 | - Under **Firewall rules**, select to **Add your client IPv4 address(xx.xx.xx.xx)**. 80 | - Check the **Allow Azure services and resources to access this server**. 81 | - Select **Save**. 82 | 83 | - Setup local MySQL connectivity 84 | - Browse to the Azure Portal. 85 | - Select your lab subscription and resource group. 86 | - Select the **python-appinsights-SUFFIX-mysql** MySQL resource. 87 | - Under **Settings**, select **Connection security**. 88 | - Select the **Yes** toggle for the **Allow access to Azure Services**. 89 | - Select **Add current client IP address**. 90 | - Select **Save**. 91 | 92 | - Setup local PostgresSQL connectivity 93 | - Browse to the Azure Portal. 94 | - Select your lab subscription and resource group. 95 | - Select the **python-appinsights-SUFFIX-mysql** PostgreSQL resource. 96 | - Under **Settings**, select **Connection security**. 97 | - Select the **Yes** toggle for the **Allow access to Azure Services**. 98 | - Select **Add client IP**. 99 | - Select **Save**. 100 | 101 | ## Configure the environment (Visual Studio Code) 102 | 103 | - Open Visual Studio code, select the **Extensions** tab. 104 | - Ensure the following extensions are installed: 105 | - [ms-python.python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) 106 | - [ms-azuretools.vscode-azurefunctions](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) 107 | 108 | ## Check your Python Version 109 | 110 | - In Visual Studio Code, open a Terminal window, run the following command: 111 | 112 | ```powershell 113 | python.exe --version 114 | ``` 115 | 116 | - For Python, make sure you see version 3.9.x. If you do not, do the following: 117 | - Download Python and change your path variables to point to the proper version. 118 | - Restart Visual Studio Code. 119 | - Open the command palette by selecting **View->Command Pallet**. 120 | - Run the `Python: Select interperter` command. 121 | - Select the `Python 3.9.x` version. 122 | 123 | ## Create a Python environment 124 | 125 | - Switch to the terminal window, ensure you are in the `azure_monitor` folder 126 | - Run the following command to create an environment where you can load all the dependencies: 127 | 128 | ```powershell 129 | py -3 -m venv .venv 130 | .venv\scripts\activate 131 | ``` 132 | 133 | - If prompted, select **yes**. 134 | - Ensure that you select the new environment in the interpertor selection otherwise the python commands you run later may not map to the proper python version. 135 | 136 | ## Install OpenCensus 137 | 138 | [OpenCensus](https://opencensus.io/) is a set of open source libraries hosted on [GitHub](https://github.com/census-instrumentation) that are used to capture trace and metric data from applications. 139 | 140 | Metrics can be things like latency, HTTP request and reponse lengths whereas distributed tracing shows how a request moves through your application layers and other services. Log data represents user generated logs that can be enriched with context and custom dimensions. Graphs can be built to show how an application or service is performing based on these data points. 141 | 142 | To install OpenCensus: 143 | 144 | - Open a terminal window, run the following: 145 | 146 | ```powershell 147 | python -m pip install --upgrade pip 148 | 149 | python -m pip install opencensus 150 | ``` 151 | 152 | ## Install Azure exporter 153 | 154 | OpenCensus needs an exporter to know how to send the log, metric and tracing data to a specific backend. Azure provides an exporter for Azure Monitor and Log Analytics. 155 | 156 | To install the Azure monitor exporter package: 157 | 158 | - Open a terminal window, run the following: 159 | 160 | ```powershell 161 | python -m pip install opencensus-ext-logging 162 | python -m pip install opencensus-ext-azure 163 | python -m pip install opencensus-extension-azure-functions 164 | python -m pip install opencensus-ext-requests 165 | python -m pip install pyodbc 166 | python -m pip install psutil 167 | ``` 168 | 169 | ## Configure environment variables 170 | 171 | - Open the `.\azure_monitor\.env` file notice that as part of the setup the values have been copied into the environment file. 172 | - Browse to the Azure Portal. 173 | - Select the Application Insights **python-appinsights-SUFFIX** resource. 174 | - On the **Overview** page, copy the connection string. Be sure to verify that the values match in the `.env` file 175 | -------------------------------------------------------------------------------- /azure_monitor/artifacts/common.ps1: -------------------------------------------------------------------------------- 1 | function GetSuffix() 2 | { 3 | $suffix = get-content "suffix.txt" -ea SilentlyContinue; 4 | 5 | if (!$suffix) 6 | { 7 | $suffix = -join ((65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_}); 8 | 9 | add-content "suffix.txt" $suffix; 10 | } 11 | 12 | return $suffix.tolower(); 13 | } 14 | function SelectSubscription() 15 | { 16 | #select a subscription 17 | $subs = Get-AzSubscription | Select-Object 18 | if($subs.GetType().IsArray -and $subs.length -gt 1){ 19 | Write-Host "You have multiple Azure subscriptions - please select the one you want to use:" 20 | for($i = 0; $i -lt $subs.length; $i++) 21 | { 22 | Write-Host "[$($i)]: $($subs[$i].Name) (ID = $($subs[$i].Id))" 23 | } 24 | $selectedIndex = -1 25 | $selectedValidIndex = 0 26 | while ($selectedValidIndex -ne 1) 27 | { 28 | $enteredValue = Read-Host("Enter 0 to $($subs.Length - 1)") 29 | if (-not ([string]::IsNullOrEmpty($enteredValue))) 30 | { 31 | if ([int]$enteredValue -in (0..$($subs.Length - 1))) 32 | { 33 | $selectedIndex = [int]$enteredValue 34 | $selectedValidIndex = 1 35 | } 36 | else 37 | { 38 | Write-Output "Please enter a valid subscription number." 39 | } 40 | } 41 | else 42 | { 43 | Write-Output "Please enter a valid subscription number." 44 | } 45 | } 46 | $selectedSub = $subs[$selectedIndex].Id 47 | Select-AzSubscription -SubscriptionId $selectedSub 48 | az account set --subscription $selectedSub 49 | } 50 | } -------------------------------------------------------------------------------- /azure_monitor/artifacts/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "suffix": { 6 | "value": "GET-SUFFIX" 7 | }, 8 | "administratorLogin": { 9 | "value": "wsuser" 10 | }, 11 | "administratorLoginPassword": { 12 | "value": "Microsoft123" 13 | }, 14 | "vmSize": { 15 | "value": "Standard_E2s_v3" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /azure_monitor/artifacts/setup.ps1: -------------------------------------------------------------------------------- 1 | . .\common.ps1 2 | 3 | #deploy the arm template 4 | Connect-AzAccount -UseDeviceAuthentication; 5 | 6 | SelectSubscription; 7 | 8 | $location = "eastus"; 9 | $suffix = GetSuffix; 10 | $resourceGroupName = "python-appinsights-$suffix"; 11 | 12 | $subscriptionId = (Get-AzContext).Subscription.Id 13 | $tenantId = (Get-AzContext).Tenant.Id 14 | $global:logindomain = (Get-AzContext).Tenant.Id; 15 | 16 | #create the resource group 17 | New-AzResourceGroup -Name $resourceGroupName -Location $location -force; 18 | 19 | #run the deployment... 20 | $templatesFile = "./template.json" 21 | $parametersFile = "./parameters.json" 22 | 23 | $content = Get-Content -Path $parametersFile -raw; 24 | $content = $content.Replace("GET-SUFFIX",$suffix); 25 | $content | Set-Content -Path "$($parametersFile).json"; 26 | 27 | $results = New-AzResourceGroupDeployment -ResourceGroupName $resourceGroupName -TemplateFile $templatesFile -TemplateParameterFile "$($parametersFile).json"; 28 | 29 | #get the values 30 | $appInsights = Get-AzApplicationInsights -ResourceGroupName $resourceGroupName -Name "python-appinsights-$suffix" 31 | 32 | $dataLakeAccountName = "storage$suffix"; 33 | $dataLakeStorageUrl = "https://"+ $dataLakeAccountName + ".dfs.core.windows.net/" 34 | $dataLakeStorageBlobUrl = "https://"+ $dataLakeAccountName + ".blob.core.windows.net/" 35 | $dataLakeStorageAccountKey = (Get-AzStorageAccountKey -ResourceGroupName $resourceGroupName -AccountName $dataLakeAccountName)[0].Value 36 | $dataLakeContext = New-AzStorageContext -StorageAccountName $dataLakeAccountName -StorageAccountKey $dataLakeStorageAccountKey 37 | 38 | $content = get-content "../env.template" 39 | $content = $content.replace("{INSIGHTS_KEY}",$appInsights.InstrumentationKey); 40 | $content = $content.replace("{INSIGHTS_CONNECTION_STRING}",$appInsights.ConnectionString); 41 | $content = $content.replace("{SUFFIX}",$suffix); 42 | $content = $content.replace("{DBUSER}","wsuser"); 43 | $content = $content.replace("{DBPASSWORD}","Microsoft123"); 44 | $content = $content.replace("{STORAGE_CONNECTION_STRING}",$dataLakeContext.ConnectionString); 45 | #$content = $content.replace("{FUNCTION_URL}",""); 46 | set-content "../.env" $content; 47 | 48 | $content = get-content "../django_sample/.env.example" 49 | $content = $content.replace("{INSIGHTS_KEY}",$appInsights.InstrumentationKey); 50 | $content = $content.replace("{INSIGHTS_CONNECTION_STRING}",$appInsights.ConnectionString); 51 | $content = $content.replace("{SUFFIX}",$suffix); 52 | $content = $content.replace("{DBUSER}","wsuser"); 53 | $content = $content.replace("{DBPASSWORD}","Microsoft123"); 54 | $content = $content.replace("{STORAGE_CONNECTION_STRING}",$dataLakeContext.ConnectionString); 55 | #$content = $content.replace("{FUNCTION_URL}",""); 56 | set-content "../django_sample/.env" $content; 57 | 58 | $content = get-content "../azfunc_sample/local.settings.json" 59 | $content = $content.replace("{INSIGHTS_KEY}",$appInsights.InstrumentationKey); 60 | $content = $content.replace("{INSIGHTS_CONNECTION_STRING}",$appInsights.ConnectionString); 61 | $content = $content.replace("{SUFFIX}",$suffix); 62 | $content = $content.replace("{DBUSER}","wsuser"); 63 | $content = $content.replace("{DBPASSWORD}","Microsoft123"); 64 | $content = $content.replace("{STORAGE_CONNECTION_STRING}",$dataLakeContext.ConnectionString); 65 | #$content = $content.replace("{FUNCTION_URL}",""); 66 | set-content "../azfunc_sample/local.settings.json" $content; 67 | 68 | #copy over the opencensus git repo... 69 | #download the git repo... 70 | Write-Host "Download Git repo." -ForegroundColor Green -Verbose 71 | git clone https://github.com/census-instrumentation/opencensus-python opencensus-python 72 | 73 | #copy the example files to the "simple_sample" folder 74 | copy ./opencensus-python/contrib/opencensus-ext-azure/examples/*/*.py ../simple_sample 75 | 76 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/BaltimoreCyberTrustRoot.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ 3 | RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD 4 | VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX 5 | DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y 6 | ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy 7 | VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr 8 | mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr 9 | IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK 10 | mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu 11 | XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy 12 | dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye 13 | jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 14 | BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 15 | DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 16 | 9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx 17 | jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 18 | Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz 19 | ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS 20 | R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp 21 | -----END CERTIFICATE----- 22 | 23 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/DigiCertGlobalRootCA.crt.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh 3 | MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 4 | d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD 5 | QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT 6 | MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j 7 | b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG 8 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB 9 | CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 10 | nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt 11 | 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P 12 | T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 13 | gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO 14 | BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR 15 | TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw 16 | DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr 17 | hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg 18 | 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF 19 | PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls 20 | YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk 21 | CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/MyHttpFunction/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import azure.functions as func 3 | import mysql.connector 4 | import ssl 5 | import os, uuid 6 | import pathlib 7 | 8 | from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient, __version__ 9 | from opencensus.extension.azure.functions import OpenCensusExtension 10 | from opencensus.trace import config_integration 11 | 12 | config_integration.trace_integrations(['mysql']) 13 | config_integration.trace_integrations(['requests']) 14 | 15 | OpenCensusExtension.configure() 16 | 17 | def main(myHttpFunction: func.HttpRequest) -> func.HttpResponse: 18 | 19 | logging.info('Python HTTP trigger function processed a request.') 20 | 21 | #sql server 22 | #server = os.getenv('DB_SERVER') 23 | #database = os.getenv('DB_DATABASE') 24 | #username = os.getenv('DB_USERNAME') 25 | #password = os.getenv('DB_PASSWORD') 26 | #driver= '{ODBC Driver 17 for SQL Server}' 27 | 28 | server = os.getenv('DBHOST') 29 | database = os.getenv('DBNAME') 30 | username = os.getenv('DBUSER') 31 | password = os.getenv('DBPASS') 32 | 33 | crtpath = 'BaltimoreCyberTrustRoot.crt.pem' 34 | #crtpath = 'DigiCertGlobalRootCA.crt.pem' 35 | 36 | ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 37 | 38 | # Connect to MySQL 39 | cnx = mysql.connector.connect( 40 | user=username, 41 | password=password, 42 | host=server, 43 | port=3306, 44 | ssl_ca=crtpath, 45 | tls_versions=['TLSv1.2'] 46 | ) 47 | 48 | logging.info(cnx) 49 | 50 | # Show databases 51 | cursor = cnx.cursor() 52 | cursor.execute("SHOW DATABASES") 53 | result_list = cursor.fetchall() 54 | 55 | # Build result response text 56 | result_str_list = [] 57 | for row in result_list: 58 | row_str = ', '.join([str(v) for v in row]) 59 | result_str_list.append(row_str) 60 | result_str = '\n'.join(result_str_list) 61 | 62 | #add a random file to storage account... 63 | connect_str = os.getenv('AZURE_STORAGE_CONNECTION_STRING') 64 | 65 | # Create the BlobServiceClient object which will be used to create a container client 66 | blob_service_client = BlobServiceClient.from_connection_string(connect_str) 67 | 68 | # Create a unique name for the container 69 | container_name = 'files' 70 | 71 | # Create the container 72 | try: 73 | container_client = blob_service_client.create_container(container_name) 74 | except: 75 | logging.info("Container exists?") 76 | 77 | # Create a local directory to hold blob data 78 | ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 79 | 80 | logging.info(ROOT_DIR) 81 | 82 | local_path = ROOT_DIR + "\\tmp\data" 83 | 84 | try: 85 | os.mkdir(local_path) 86 | except: 87 | logging.info("Folder exists?") 88 | 89 | # Create a file in the local data directory to upload and download 90 | local_file_name = str(uuid.uuid4()) + ".txt" 91 | upload_file_path = os.path.join(local_path, local_file_name) 92 | 93 | # Write text to the file 94 | file = open(upload_file_path, 'w') 95 | file.write("Hello, World!") 96 | file.close() 97 | 98 | # Create a blob client using the local file name as the name for the blob 99 | blob_client = blob_service_client.get_blob_client(container=container_name, blob=local_file_name) 100 | 101 | logging.info("\nUploading to Azure Storage as blob:\n\t" + local_file_name) 102 | 103 | # Upload the created file 104 | with open(upload_file_path, "rb") as data: 105 | blob_client.upload_blob(data) 106 | 107 | 108 | return func.HttpResponse( 109 | result_str, 110 | status_code=200 111 | ) -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/MyHttpFunction/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "__init__.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "function", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "myHttpFunction", 9 | "methods": [ 10 | "get", 11 | "post" 12 | ] 13 | }, 14 | { 15 | "type": "http", 16 | "direction": "out", 17 | "name": "$return" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/MyHttpFunction/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Sample: Configuring Function Apps with Python and OpenCensus' 3 | description: Create Azure Functions using Python and OpenCensus. 4 | author: givenscj 5 | ms.author: givenscj 6 | ms.devlang: python 7 | ms.topic: sample 8 | ms.date: 07/27/2022 9 | ms.custom: [] 10 | --- 11 | 12 | # Sample: Configuring Function Apps with OpenCensus SDK 13 | 14 | This is an example of how to configure Azure Function applications to send dependencies calls to Azure Monitor. You will run the samples both locally and deploy the functions to an Azure Functions resource you deployed as part of the setup script. These Function Apps sample will utilize OpenCensus depencencies to record calls to external services such as internal method calls, MySQL and Azure Storage. 15 | 16 | ## 1 - Environment Setup 17 | 18 | Follow the steps in the [documentation](/azure_monitor/README.md) to setup your environment. 19 | 20 | ## 2 - Utilize Function Apps with OpenCensus 21 | 22 | - Start the Azure Storage Emulator. 23 | - Switch to Visual Studio Code. 24 | - Open a terminal window, run the following commands to start the Python functions locally: 25 | 26 | ```powershell 27 | cd .\azure_monitor\azfunc_sample 28 | 29 | python -m pip install -r requirements.txt 30 | 31 | func start 32 | ``` 33 | 34 | > NOTE: The `func` command is installed as part of the Azure Function Tools as part of the environment setup. 35 | 36 | - If you get an error about `psutil._psutil_windows` do the following: 37 | - Open the `.venv\Lib\site-packages` folder and delete the `psutil` and `psutil-5.9.1.dist-info` folders. 38 | - Then run the following: 39 | 40 | ```Python 41 | python -m pip install --upgrade pip 42 | python -m pip install psutil 43 | ``` 44 | 45 | - Browse to the HTTP function endpoint `http://localhost:7071/api/MyHttpFunction`, you can review the code in the `\azfunc_sample\MyHttpFunction\__init__.py`, you will see that it performs the following: 46 | - Connects to MySQL and performs a query 47 | - Creates a file an uploads it to Azure Storage 48 | 49 | ## 3 - Deploy to Azure 50 | 51 | - Run the following command to deploy the function apps to Azure. Be sure to replace the `SUFFIX` below (you can find it in the deployed Azure Portal resource group or in the `.\azure_monitor\artifacts\suffix.txt` file that was generated by the setup script): 52 | 53 | ```powershell 54 | cd .\azure_monitor\azfunc_sample 55 | 56 | az login 57 | 58 | func azure functionapp publish python-appinsights-SUFFIX 59 | ``` 60 | 61 | - You should see the function applications deployed successfully: 62 | 63 | ![The results of the function app publish command is displayed.](../media/python_functionapp_deploy.png "Review the results and ensure the function apps deployed succesfully.") 64 | 65 | - Switch to the Azure Portal. 66 | - Navigate to the **python-appinsights-SUFFIX** function app. 67 | - Under **Functions**, select **Functions**, you should see the two python function apps deployed. 68 | 69 | > **NOTE** Azure Function runtimes support various Python versions depending on the version selected. See [Languages by runtime version](https://docs.microsoft.com/azure/azure-functions/supported-languages#languages-by-runtime-version). 70 | 71 | - Review the function app code. Notice the usage of the a storage account and a call to the database. 72 | 73 | ## 4 - Review Dependency Log Data 74 | 75 | - Navigate to the **python-appinsights-SUFFIX** Application Insights resource, under **Monitoring**, select **Logs** 76 | - Run the following query: 77 | 78 | ```kusto 79 | dependencies 80 | ``` 81 | 82 | - You should see the dependencies displayed for the calls to MySQL and Azure storage displayed. 83 | 84 | ![The results of the query is displayed.](../media/python_azure_dependencies.png "Review the query output for the dependencies.") 85 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/docs/assets/01-ai-correlated-invocation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/azfunc_sample/docs/assets/01-ai-correlated-invocation.PNG -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/docs/assets/02-ai-correlated-invocation.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/azfunc_sample/docs/assets/02-ai-correlated-invocation.PNG -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/docs/assets/03-ai-application-map.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/azfunc_sample/docs/assets/03-ai-application-map.PNG -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[2.*, 3.0.0)" 6 | } 7 | } -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/__init__.py: -------------------------------------------------------------------------------- 1 | # the code in globals ensures that logging 2 | # is initialized at the very beginning 3 | from . import globals # noqa: F401 4 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "scriptFile": "main.py", 3 | "bindings": [ 4 | { 5 | "authLevel": "anonymous", 6 | "type": "httpTrigger", 7 | "direction": "in", 8 | "name": "req", 9 | "methods": [ 10 | "get" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "$return" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/globals.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import logging 4 | 5 | from logging import Logger 6 | from opencensus.trace import config_integration 7 | from opencensus.ext.azure.log_exporter import AzureLogHandler 8 | 9 | 10 | AI_CONNECTION_STRING = os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING") 11 | 12 | if not AI_CONNECTION_STRING: 13 | raise EnvironmentError( 14 | "AI Connection string not set. Set it through the environment variable: APPLICATIONINSIGHTS_CONNECTION_STRING." 15 | ) 16 | 17 | WEBSITE_SITE_NAME = os.getenv("WEBSITE_SITE_NAME") 18 | 19 | if not WEBSITE_SITE_NAME: 20 | raise EnvironmentError( 21 | "Website site name not set. Set it through the environment variable: WEBSITE_SITE_NAME." 22 | ) 23 | 24 | EXTERNAL_DEPENDENCY_URL = os.getenv("EXTERNAL_DEPENDENCY_URL") 25 | 26 | if not EXTERNAL_DEPENDENCY_URL: 27 | raise EnvironmentError( 28 | "External dependency URL not set. Set it through the environment variable: EXTERNAL_DEPENDENCY_URL." 29 | ) 30 | 31 | # initialize observability (with opencensus) 32 | config_integration.trace_integrations(["logging", "requests"]) 33 | 34 | 35 | def callback_add_role_name(envelope): 36 | """Add role name for logger.""" 37 | envelope.tags["ai.cloud.role"] = WEBSITE_SITE_NAME 38 | envelope.tags["ai.cloud.roleInstance"] = socket.getfqdn() 39 | 40 | 41 | def getLogger( 42 | name: str, 43 | instrumentation_conn_string: str = AI_CONNECTION_STRING, 44 | propagate: bool = False, 45 | ) -> Logger: 46 | """Get a new logging instance with a handler to send logs to Application Insights 47 | 48 | Args: 49 | name([str]): [The name of the logger] 50 | instrumentation_conn_string([str]): [The AppInsights instrumentation connection string] 51 | propagate([bool]): [Enable log propagation (default: false)] 52 | """ 53 | logHandler = AzureLogHandler(connection_string=instrumentation_conn_string) 54 | logHandler.add_telemetry_processor(callback_add_role_name) 55 | 56 | logger = logging.getLogger(name) 57 | logger.addHandler(logHandler) 58 | logger.propagate = propagate 59 | 60 | return logger 61 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/instrumentation_func.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | from .globals import getLogger, EXTERNAL_DEPENDENCY_URL 4 | from .utils import call_internal_api, call_external_api 5 | 6 | logger = getLogger(__name__) 7 | 8 | 9 | class FunctionLogic: 10 | @classmethod 11 | def run(cls, req: func.HttpRequest) -> func.HttpResponse: 12 | """Azure Function business logic 13 | 14 | Args: 15 | req([HttpRequest]): [Incoming HTTP request] 16 | """ 17 | logger.info("new invocation received") 18 | 19 | # TRACK DEPENDENCY (IN-PROC): 20 | # This long running code is logged as a dependency 21 | # it uses a method decorator 22 | call_internal_api(delay=3.0) 23 | 24 | # TRACK DEPENDENCY (HTTP): 25 | # This uses Opencensus' requests extension 26 | call_external_api(url=EXTERNAL_DEPENDENCY_URL) 27 | 28 | # TRACES (SEVERITY) 29 | # create log entries with different severity levels (warning, exception) 30 | logger.warning("log warning message") 31 | 32 | try: 33 | assert 1 == 0 34 | except Exception as ex: 35 | logger.exception(ex) 36 | 37 | return func.HttpResponse( 38 | "This HTTP triggered function executed successfully.", 39 | status_code=200, 40 | ) 41 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/main.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | from opencensus.extension.azure.functions import OpenCensusExtension 3 | 4 | from .globals import AI_CONNECTION_STRING, callback_add_role_name, getLogger 5 | from .instrumentation_func import FunctionLogic 6 | 7 | # configure opencensus ext for azure function. 8 | # this ensures that the an opencensus tracer is created and associated with the func context 9 | OpenCensusExtension.configure(connection_string=AI_CONNECTION_STRING) 10 | 11 | # ensure that dependency records have the correct role name 12 | OpenCensusExtension._exporter.add_telemetry_processor(callback_add_role_name) 13 | 14 | logger = getLogger(__name__) 15 | 16 | 17 | def main(req: func.HttpRequest, context: func.Context) -> func.HttpResponse: 18 | """Azure Function entry point 19 | 20 | Args: 21 | req([HttpRequest]): [Incoming HTTP request] 22 | context([Context]): [Azure function invocation context] 23 | """ 24 | logger.info("Python HTTP trigger function processed a request.") 25 | 26 | return FunctionLogic.run(req) 27 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/instrumentation/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from functools import wraps 4 | from opencensus.trace import execution_context 5 | from opencensus.trace.tracer import Tracer 6 | 7 | from .globals import getLogger 8 | 9 | logger = getLogger(__name__) 10 | 11 | 12 | def trace_as_dependency(tracer: Tracer = None, name: str = None): 13 | """trace_as_dependency [method decorator to trace a method invocation as a dependency (in AppInsights)] 14 | 15 | Args: 16 | tracer (Tracer): [Opencensus tracer object used to create the trace record.] 17 | name (str): [Name of the created trace record] 18 | 19 | Returns: 20 | The inner function 21 | """ 22 | 23 | def inner_function(method): 24 | @wraps(method) 25 | def wrapper(*args, **kwargs): 26 | trace_name = name if (name is not None) else method.__name__ 27 | oc_tracer = ( 28 | tracer 29 | if (tracer is not None) 30 | else execution_context.get_opencensus_tracer() 31 | ) 32 | 33 | if oc_tracer is not None: 34 | with oc_tracer.span(trace_name): 35 | result = method(*args, **kwargs) 36 | else: 37 | result = method(*args, **kwargs) 38 | 39 | return result 40 | 41 | return wrapper 42 | 43 | return inner_function 44 | 45 | 46 | @trace_as_dependency(name="long running api call") 47 | def call_internal_api(delay: float) -> None: 48 | """Dummy API call to demonstrate dependecy tracking (IN-PROC) 49 | 50 | Args: 51 | delay([float]): [Delay method execution in sec] 52 | """ 53 | time.sleep(delay) 54 | 55 | 56 | def call_external_api(url: str) -> None: 57 | """Dummy API call to demonstrate dependecy tracking (HTTP) 58 | 59 | Args: 60 | url([str]): [URL to invoke] 61 | """ 62 | requests.get(url) 63 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "FUNCTIONS_WORKER_RUNTIME": "python", 5 | "DBHOST": "python-appinsights-{SUFFIX}-mysql.mysql.database.azure.com", 6 | "DBNAME": "python", 7 | "DBUSER": "{DBUSER}@python-appinsights-{SUFFIX}-mysql", 8 | "DBPASS": "{DBPASSWORD}", 9 | "AZURE_STORAGE_CONNECTION_STRING" : "{STORAGE_CONNECTION_STRING}", 10 | "APPLICATIONINSIGHTS_CONNECTION_STRING" : "{INSIGHTS_CONNECTION_STRING}", 11 | "AzureWebJobsStorage" : "UseDevelopmentStorage=true", 12 | "WEBSITE_SITE_NAME" : "instrumentation", 13 | "EXTERNAL_DEPENDENCY_URL" : "https://www.bing.com" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/requirements.txt: -------------------------------------------------------------------------------- 1 | opencensus 2 | opencensus-ext-logging 3 | opencensus-ext-azure 4 | opencensus-extension-azure-functions 5 | opencensus-ext-requests 6 | opencensus-ext-mysql 7 | azure-functions 8 | requests 9 | mysql 10 | mysql-connector-python 11 | pyodbc 12 | azure-storage-blob 13 | psutil 14 | cffi 15 | ffi 16 | pytest -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/azfunc_sample/tests/__init__.py -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tests/test_globals.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from opencensus.ext.azure.log_exporter import AzureLogHandler 4 | from opencensus.log import TraceLogger 5 | 6 | from instrumentation.globals import getLogger 7 | 8 | CONNECTION_STRING = "InstrumentationKey=00000000-0000-0000-0000-000000000000" 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "name, conn_string, propagate", 13 | [(__name__, CONNECTION_STRING, False), ("test", CONNECTION_STRING, True)], 14 | ) 15 | def test_logger_correctly_configured( 16 | name: str, conn_string: str, propagate: bool 17 | ) -> None: 18 | sut = getLogger( 19 | name=name, instrumentation_conn_string=conn_string, propagate=propagate 20 | ) 21 | 22 | assert sut.name == name 23 | assert sut.propagate == propagate 24 | assert isinstance(sut, TraceLogger) 25 | assert len(sut.handlers) == 1 26 | assert isinstance(sut.handlers[0], AzureLogHandler) 27 | assert sut.handlers[0].options["connection_string"] == CONNECTION_STRING 28 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tests/test_instrumentation_func.py: -------------------------------------------------------------------------------- 1 | import azure.functions as func 2 | 3 | from instrumentation.instrumentation_func import FunctionLogic 4 | 5 | 6 | def create_http_request() -> func.HttpRequest: 7 | # Construct a mock HTTP request. 8 | req = func.HttpRequest( 9 | method="GET", 10 | body="", 11 | url="/api/sample", 12 | ) 13 | return req 14 | 15 | 16 | def test_run_successful() -> None: 17 | http_req = create_http_request() 18 | http_resp = FunctionLogic.run(http_req) 19 | 20 | assert http_resp.status_code == 200 21 | -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tmp/data/05582fa0-2f83-48ca-96ae-8206ee59aab5.txt: -------------------------------------------------------------------------------- 1 | Hello, World! -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tmp/data/a20f2b98-bd5a-4692-ab93-397a460d722f.txt: -------------------------------------------------------------------------------- 1 | Hello, World! -------------------------------------------------------------------------------- /azure_monitor/azfunc_sample/tmp/data/bd7ae612-ca6a-4bb4-a146-9926f24a4e10.txt: -------------------------------------------------------------------------------- 1 | Hello, World! -------------------------------------------------------------------------------- /azure_monitor/django_sample/.env.example: -------------------------------------------------------------------------------- 1 | APPINSIGHTS_INSTRUMENTATIONKEY={INSIGHTS_KEY} 2 | APPLICATIONINSIGHTS_CONNECTION_STRING={INSIGHTS_CONNECTION_STRING} 3 | STORAGE_ACCOUNT_NAME=storage{SUFFIX} 4 | 5 | DB_SERVER=python-appinsights-{SUFFIX}.database.windows.net 6 | DB_DATABASE=python 7 | DB_USERNAME={DBUSER} 8 | DB_PASSWORD={DBPASSWORD} 9 | DB_PORT = 1433 10 | 11 | DBHOST=python-appinsights-{SUFFIX}-mysql.mysql.database.azure.com 12 | DBNAME=python 13 | DBUSER={DBUSER}@python-appinsights-{SUFFIX}-mysql 14 | DBPASS={DBPASSWORD} 15 | 16 | PGDBHOST=python-appinsights-{SUFFIX}-pg.postgres.database.azure.com 17 | PGDBNAME=python 18 | PGDBUSER={DBUSER}@python-appinsights-{SUFFIX}-pg 19 | PGDBPASS={DBPASSWORD} 20 | 21 | AZURE_STORAGE_CONNECTION_STRING={STORAGE_CONNECTION_STRING} 22 | FUNCTION_URL={FUNCTION_URL} 23 | 24 | # Blob storage connection info 25 | STORAGE_URL=https://storage{SUFFIX}.blob.core.windows.net 26 | STORAGE_CONTAINER_NAME=photos 27 | 28 | #WEBSITE_HOSTNAME=http://127.0.0.1:8000 -------------------------------------------------------------------------------- /azure_monitor/django_sample/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv* 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | __azurite* 113 | .deployment 114 | 115 | # Keys 116 | *.pem 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Sample: Configuring Django applications with OpenCensus' 3 | description: Configuring Django applications with OpenCensus. 4 | author: givenscj 5 | ms.author: givenscj 6 | ms.devlang: python 7 | ms.topic: sample 8 | ms.date: 07/27/2022 9 | ms.custom: [] 10 | --- 11 | 12 | # Sample: Configuring Django applications with OpenCensus 13 | 14 | This next example shows how to add the various OpenCensus settings to a Django application to send logging and metric data to Azure Monitor. You will run the samples both locally and deploy the Django web application to the Azure App Service resource you deployed as part of the setup script. 15 | 16 | ## 1 - Environment Setup 17 | 18 | Follow the steps in the [setup documentation](/azure_monitor/README.md). 19 | 20 | ## 2 - Configure Environment 21 | 22 | - Run the following commands to setup the environment: 23 | 24 | ```powershell 25 | cd $home 26 | 27 | cd .\azure_monitor\django_sample 28 | ``` 29 | 30 | - Review the `requirements.txt` file, you will see all the required packages for the sample included. Exercute the following command to install all the packages in the requirements.txt file: 31 | 32 | ```python 33 | python -m pip install -r requirements.txt 34 | ``` 35 | 36 | - The following items have been done for you in the ARM template: 37 | - System assigned managed identity for the app service is enabled. 38 | - Storage Blob Data Contributor has been assigned to the managed identity. 39 | 40 | - Setup local PostgreSQL connectivity 41 | - Browse to the Azure Portal. 42 | - Select your lab subscription and resource group. 43 | - Select the **python-appinsights-SUFFIX-pg** Postgres SQL resource. 44 | - Under **Settings**, select **Connection security**. 45 | - Select **Add client IP**. 46 | - Select **Save**. 47 | 48 | - Setup the Application insights middleware 49 | - Open the `./azure_monitor/django_sample/azureproject/settings.py` file. 50 | - Ensure the following line is present in the `MIDDLEWARE` section: 51 | 52 | ```python 53 | 'opencensus.ext.django.middleware.OpencensusMiddleware', 54 | ``` 55 | 56 | -- Ensure the following `OPENCENSUS` section is present in the `settings.py` file. 57 | 58 | ```python 59 | OPENCENSUS = { 60 | 'TRACE': { 61 | 'SAMPLER': 'opencensus.trace.samplers.ProbabilitySampler(rate=1)', 62 | 'EXPORTER': 'opencensus.ext.azure.trace_exporter.AzureExporter(connection_string="' + appKey + '")', 63 | } 64 | } 65 | ``` 66 | 67 | ## 3 - Create Database 68 | 69 | - Create the database schema: 70 | 71 | ```python 72 | py manage.py migrate 73 | ``` 74 | 75 | - If you get an error about `psutil._psutil_windows` do the following: 76 | - Open the `\azure-monitor\.venv\Lib\site-packages` folder and delete the `psutil` and `psutil-5.9.1.dist-info` folders. 77 | - Then run the following: 78 | 79 | ```Python 80 | python -m pip install --upgrade pip 81 | python -m pip install psutil 82 | ``` 83 | 84 | ## 4 - Test Application 85 | 86 | - Run the application and test the logging. 87 | 88 | ```python 89 | python manage.py runserver 90 | ``` 91 | 92 | - Open a web browser to `http://127.0.0.1:8000/`. 93 | - Add a new resturant. 94 | - Select **Add new resturant**. 95 | - For the name, type `Contoso BBQ`. 96 | - For the address, type `1 Microsoft Way`. 97 | - For the description, type `BBQ`. 98 | - Select **Submit**, the resturant will be created. 99 | - Select **Add new review**. 100 | - Type a name. 101 | - Select a rating. 102 | - Add a comment. 103 | - Choose a random picture. 104 | - Select **Save changes**. 105 | 106 | ## 4 - Review Log Data 107 | 108 | - Switch to the Azure Portal. 109 | - Browse to the **python-applicationinsights-SUFFIX** Application Insights resource. 110 | - Under **Monitoring**, select **Logs**. 111 | - In the query window, type the following: 112 | 113 | ```kql 114 | requests 115 | | where cloud_RoleName == "manage.py" 116 | ``` 117 | 118 | - Select **Run**, you should see the following: 119 | 120 | ![The query is displayed with the results from web app request data.](../media/python_webapp_requests.png "Review the results of the query.") 121 | 122 | - Select the **Azure** icon in the toolbar of Visual Studio code. 123 | - If prompted, select **Sign in to Azure..** and follow the prompts. 124 | - Expand **Resources**, if needed, sign-in to Azure. 125 | - Expand your target lab subscription. 126 | - Expand **App Services**. 127 | - Locate the target web app to deploy too, this will be **python-appinsights-SUFFIX-app**. 128 | - If needed, select **Install extension to enable additional features**, then select **Install** to instal the Azure App Service extension. 129 | - In Visual Studio Code, right-click the **django_sample** folder, select **Deploy to web app**. 130 | - Selec the **python-appinsights-SUFFIX-app** web app. 131 | - If prompted, select **Deploy**. 132 | - Browse to the **python-appinsights-SUFFIX-app.azurewebsites.net** web site, the site should load successfully. 133 | - Add some more resturants and reviews, this will cause log data to be sent to Azure Monitor via OpenCensus. 134 | 135 | ## 5 - Review Custom Metrics 136 | 137 | For the sample resturant application, you have two places where metrics are sent to Application Insights. 138 | 139 | - Single Value - The number of resturants created. 140 | - Aggregation - Page views over a one minute time period. 141 | 142 | - Switch to the Azure Portal and the application insights instance. 143 | - Under **Monitoring**, select **Metrics**. 144 | - For the **Metric namespace**, select **reviews_view**. 145 | - For the **Metric**, select **reviews_view**. 146 | - You should see some data displayed for the reviews made from the web site: 147 | 148 | ![The custom metric for the reviews view is displayed.](../media/python_custommetrics-web-reviews.png "Review the results of the metric data.") 149 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/azureproject/__init__.py -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/app_insights.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from opencensus.ext.azure.log_exporter import AzureLogHandler 5 | from opencensus.ext.azure import metrics_exporter 6 | from opencensus.stats import aggregation as aggregation_module 7 | from opencensus.stats import measure as measure_module 8 | from opencensus.stats import stats as stats_module 9 | from opencensus.stats import view as view_module 10 | from opencensus.tags import tag_map as tag_map_module 11 | 12 | stats = stats_module.stats 13 | view_manager = stats.view_manager 14 | stats_recorder = stats.stats_recorder 15 | 16 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 17 | 18 | REST_MEASURE = measure_module.MeasureInt("resturants", 19 | "number of resturants", 20 | "resturants") 21 | REST_VIEW = view_module.View("rest_view", 22 | "number of resturants", 23 | ["state"], 24 | REST_MEASURE, 25 | aggregation_module.CountAggregation()) 26 | 27 | ORDERS_MEASURE = measure_module.MeasureInt("orders", 28 | "number of orders", 29 | "orders") 30 | 31 | ORDERS_VIEW = view_module.View("orders_view", 32 | "number of orders", 33 | [], 34 | ORDERS_MEASURE, 35 | aggregation_module.CountAggregation()) 36 | 37 | REVIEWS_MEASURE = measure_module.MeasureInt("reviews","number of reviews","reviews") 38 | 39 | REVIEWS_VIEW = view_module.View("reviews_view", 40 | "number of reviews", 41 | ["resturantId"], 42 | REVIEWS_MEASURE, 43 | aggregation_module.CountAggregation()) 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | logger.addHandler(AzureLogHandler( 48 | connection_string=appKey) 49 | ) 50 | 51 | def register_views(): 52 | 53 | stats = stats_module.stats 54 | view_manager = stats.view_manager 55 | 56 | exporter = metrics_exporter.new_metrics_exporter(connection_string=appKey) 57 | view_manager.register_exporter(exporter) 58 | 59 | view_manager.register_view(REVIEWS_VIEW) 60 | view_manager.register_view(ORDERS_VIEW) 61 | view_manager.register_view(REST_VIEW) 62 | 63 | def record_metric_pageviews(): 64 | mmap = stats_recorder.new_measurement_map() 65 | tmap = tag_map_module.TagMap() 66 | mmap.measure_int_put(REVIEWS_MEASURE, 1) 67 | mmap.record(tmap) 68 | logger.info("metrics: %s value: %s number of measurements: %s ",REVIEWS_MEASURE.name, 1, len(mmap.measurement_map)) 69 | 70 | def record_metric_review(tmap): 71 | mmap = stats_recorder.new_measurement_map() 72 | mmap.measure_int_put(REVIEWS_MEASURE, 1) 73 | mmap.record(tmap) 74 | logger.info("metrics: %s value: %s number of measurements: %s ",REVIEWS_MEASURE.name, 1, len(mmap.measurement_map)) 75 | 76 | def record_metric_resturant(tmap): 77 | mmap = stats_recorder.new_measurement_map() 78 | mmap.measure_int_put(REST_MEASURE, 1) 79 | mmap.record(tmap) 80 | logger.info("metrics: %s value: %s number of measurements: %s ",REVIEWS_MEASURE.name, 1, len(mmap.measurement_map)) 81 | 82 | def record_metric_order(): 83 | mmap = stats_recorder.new_measurement_map() 84 | tmap = tag_map_module.TagMap() 85 | mmap.measure_int_put(ORDERS_MEASURE, 1) 86 | mmap.record(tmap) 87 | logger.info("metrics: %s value: %s number of measurements: %s ",REVIEWS_MEASURE.name, 1, len(mmap.measurement_map)) 88 | 89 | def record_metric_float(mmap,value,measure): 90 | # data from the speed test 91 | mmap.measure_float_put(measure,value) 92 | # the measure becomes the key to the measurement map 93 | logger.info("metrics: %s value: %s number of measurements: %s ",measure.name, value, len(mmap.measurement_map)) 94 | 95 | 96 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for azureproject project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'azureproject.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/development.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .settings import * 3 | from .get_token import get_token 4 | from azureproject.app_insights import * 5 | from opencensus.trace import config_integration 6 | 7 | config_integration.trace_integrations(['postgresql']) 8 | config_integration.trace_integrations(['requests']) 9 | 10 | # SECURITY WARNING: don't run with debug turned on in production! 11 | DEBUG = True 12 | 13 | # Don't use Whitenoise to avoid having to run collectstatic first. 14 | STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.StaticFilesStorage' 15 | 16 | ALLOWED_HOSTS = ['*'] 17 | 18 | # Configure Postgres database for local development 19 | # Set these environment variables in the .env file for this project. 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'django.db.backends.postgresql', 23 | 'NAME': os.environ['PGDBNAME'], 24 | 'HOST': os.environ['PGDBHOST'], 25 | 'USER': os.environ['PGDBUSER'], 26 | 'OPTIONS': {'sslmode': 'require'}, 27 | 'PASSWORD': os.environ['PGDBPASS'] 28 | } 29 | } 30 | 31 | #load all the custom metrics... 32 | register_views() 33 | 34 | #get_token() 35 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/get_token.py: -------------------------------------------------------------------------------- 1 | import os 2 | from azure.identity import DefaultAzureCredential 3 | import django.conf as conf 4 | 5 | def get_token(): 6 | if 'WEBSITE_HOSTNAME' in os.environ: 7 | # Azure hosted, refresh token that becomes password. 8 | azure_credential = DefaultAzureCredential() 9 | # Get token for Azure Database for PostgreSQL 10 | token = azure_credential.get_token("https://ossrdbms-aad.database.windows.net") 11 | #conf.settings.DATABASES['default']['PASSWORD'] = token.token 12 | conf.settings.DATABASES['default']['PASSWORD'] = os.environ['DBPASS'] 13 | else: 14 | # Locally, read password from environment variable. 15 | conf.settings.DATABASES['default']['PASSWORD'] = os.environ['DBPASS'] 16 | print("Read password env variable.") 17 | return -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/production.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .settings import * 3 | from .get_token import get_token 4 | from azureproject.app_insights import * 5 | from opencensus.trace import config_integration 6 | 7 | config_integration.trace_integrations(['postgresql']) 8 | config_integration.trace_integrations(['requests']) 9 | 10 | # Configure the domain name using the environment variable 11 | # that Azure automatically creates for us. 12 | ALLOWED_HOSTS = [os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else [] 13 | 14 | CSRF_TRUSTED_ORIGINS = ['https://'+ os.environ['WEBSITE_HOSTNAME']] if 'WEBSITE_HOSTNAME' in os.environ else [] 15 | DEBUG = False 16 | DEBUG_PROPAGATE_EXCEPTIONS = True 17 | 18 | STATICFILES_STORAGE = 'whitenoise.storage.CompressedStaticFilesStorage' 19 | 20 | # DBHOST is only the server name, not the full URL 21 | #hostname = os.environ['DBHOST'] 22 | #username = os.environ['DBUSER'] + "@" + os.environ['DBHOST'] 23 | 24 | # Configure Postgres database; the full username for PostgreSQL flexible server is 25 | # username (not @sever-name). 26 | DATABASES = { 27 | 'default': { 28 | 'ENGINE': 'django.db.backends.postgresql', 29 | 'NAME': os.environ['PGDBNAME'], 30 | 'HOST': os.environ['PGDBHOST'], 31 | 'USER': os.environ['PGDBUSER'], 32 | 'OPTIONS': {'sslmode': 'require'}, 33 | 'PASSWORD': os.environ['PGDBPASS'] 34 | } 35 | } 36 | 37 | #load all the custom metrics... 38 | register_views() 39 | 40 | #get_token() 41 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/settings.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | 4 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 5 | BASE_DIR = Path(__file__).resolve().parent.parent 6 | 7 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 'django-insecure-7ppocbnx@w71dcuinn*t^_mzal(t@o01v3fee27g%rg18fc5d@' 11 | 12 | # Application definition 13 | INSTALLED_APPS = [ 14 | 'restaurant_review.apps.RestaurantReviewConfig', 15 | 'django.contrib.admin', 16 | 'django.contrib.auth', 17 | 'django.contrib.contenttypes', 18 | 'django.contrib.sessions', 19 | 'django.contrib.messages', 20 | 'whitenoise.runserver_nostatic', 21 | 'django.contrib.staticfiles', 22 | 'sslserver' 23 | ] 24 | 25 | MIDDLEWARE = [ 26 | 'django.middleware.security.SecurityMiddleware', 27 | 'whitenoise.middleware.WhiteNoiseMiddleware', 28 | 'django.contrib.sessions.middleware.SessionMiddleware', 29 | 'django.middleware.common.CommonMiddleware', 30 | 'django.middleware.csrf.CsrfViewMiddleware', 31 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 32 | 'django.contrib.messages.middleware.MessageMiddleware', 33 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 34 | 'opencensus.ext.django.middleware.OpencensusMiddleware' 35 | ] 36 | 37 | OPENCENSUS = { 38 | 'TRACE': { 39 | 'SAMPLER': 'opencensus.trace.samplers.ProbabilitySampler(rate=1)', 40 | 'EXPORTER': 'opencensus.ext.azure.trace_exporter.AzureExporter(connection_string="' + appKey + '")' 41 | } 42 | } 43 | 44 | # Static files (CSS, JavaScript, Images) 45 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 46 | 47 | STATIC_URL = 'static/' 48 | STATICFILES_DIRS = (str(BASE_DIR.joinpath('static')),) 49 | STATIC_ROOT = str(BASE_DIR.joinpath('staticfiles')) 50 | 51 | WHITENOISE_USE_FINDERS = True 52 | WHITENOISE_AUTOREFRESH = True 53 | WHITENOISE_MANIFEST_STRICT = False 54 | 55 | ROOT_URLCONF = 'azureproject.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' 74 | 75 | WSGI_APPLICATION = 'azureproject.wsgi.application' 76 | 77 | AUTH_PASSWORD_VALIDATORS = [ 78 | { 79 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 80 | }, 81 | { 82 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 83 | }, 84 | { 85 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 86 | }, 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 89 | }, 90 | ] 91 | 92 | 93 | # Internationalization 94 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 95 | 96 | LANGUAGE_CODE = 'en-us' 97 | TIME_ZONE = 'UTC' 98 | USE_I18N = True 99 | USE_TZ = True 100 | 101 | # Default primary key field type 102 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 103 | 104 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 105 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/urls.py: -------------------------------------------------------------------------------- 1 | """azureproject URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | 19 | urlpatterns = [ 20 | path('', include('restaurant_review.urls')), 21 | path('admin/', admin.site.urls), 22 | ] 23 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/azureproject/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for azureproject project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | # Check for the WEBSITE_HOSTNAME environment variable to see if we are running in Azure App Service 15 | # If so, then load the settings from production.py 16 | settings_module = 'azureproject.production' if 'WEBSITE_HOSTNAME' in os.environ else 'azureproject.settings' 17 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module) 18 | 19 | application = get_wsgi_application() 20 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | from dotenv import load_dotenv 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | # If WEBSITE_HOSTNAME is defined as an environment variable, then we're running on Azure App Service 10 | 11 | # Only for Local Development - Load environment variables from the .env file 12 | if not 'WEBSITE_HOSTNAME' in os.environ: 13 | print("Loading environment variables for .env file") 14 | load_dotenv('./.env') 15 | 16 | # When running on Azure App Service you should use the production settings. 17 | settings_module = "azureproject.production" if 'WEBSITE_HOSTNAME' in os.environ else 'azureproject.development' 18 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module) 19 | 20 | try: 21 | from django.core.management import execute_from_command_line 22 | except ImportError as exc: 23 | raise ImportError( 24 | "Couldn't import Django. Are you sure it's installed and " 25 | "available on your PYTHONPATH environment variable? Did you " 26 | "forget to activate a virtual environment?" 27 | ) from exc 28 | execute_from_command_line(sys.argv) 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | psycopg2-binary 3 | python-dotenv 4 | whitenoise 5 | django-sslserver 6 | azure-storage-blob 7 | azure-identity 8 | opencensus 9 | opencensus-ext-azure 10 | opencensus-ext-django 11 | opencensus-ext-postgresql 12 | psutil 13 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/restaurant_review/__init__.py -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Restaurant, Review 3 | 4 | # Register your models here. 5 | 6 | admin.site.register(Restaurant) 7 | admin.site.register(Review) 8 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RestaurantReviewConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'restaurant_review' 7 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.2 on 2022-02-08 04:34 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Restaurant', 18 | fields=[ 19 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('name', models.CharField(max_length=50)), 21 | ('street_address', models.CharField(max_length=50)), 22 | ('description', models.CharField(max_length=250)), 23 | ], 24 | ), 25 | migrations.CreateModel( 26 | name='Review', 27 | fields=[ 28 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('user_name', models.CharField(max_length=20)), 30 | ('rating', models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)])), 31 | ('review_text', models.CharField(max_length=500)), 32 | ('review_date', models.DateTimeField(verbose_name='review date')), 33 | ('restaurant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='restaurant_review.restaurant')), 34 | ], 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0002_alter_review_rating.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-18 14:51 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('restaurant_review', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='review', 16 | name='rating', 17 | field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(5)]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0003_restaurant_image_name_review_image_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-18 15:16 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('restaurant_review', '0002_alter_review_rating'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='restaurant', 16 | name='image_name', 17 | field=models.UUIDField(default=uuid.UUID('24ba6836-5acb-4e64-961a-b994de8cfb01')), 18 | ), 19 | migrations.AddField( 20 | model_name='review', 21 | name='image_name', 22 | field=models.UUIDField(default=uuid.UUID('58977b57-8293-4a84-bccc-34ef7bb138a4')), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0004_alter_restaurant_image_name_alter_review_image_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-18 16:29 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('restaurant_review', '0003_restaurant_image_name_review_image_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='restaurant', 15 | name='image_name', 16 | field=models.UUIDField(), 17 | ), 18 | migrations.AlterField( 19 | model_name='review', 20 | name='image_name', 21 | field=models.UUIDField(), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0005_remove_restaurant_image_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-19 16:12 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('restaurant_review', '0004_alter_restaurant_image_name_alter_review_image_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='restaurant', 15 | name='image_name', 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0006_alter_review_image_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-19 17:03 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('restaurant_review', '0005_remove_restaurant_image_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='review', 15 | name='image_name', 16 | field=models.CharField(max_length=100), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/0007_alter_review_image_name.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.0.4 on 2022-04-20 11:11 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('restaurant_review', '0006_alter_review_image_name'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='review', 15 | name='image_name', 16 | field=models.CharField(max_length=100, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/restaurant_review/migrations/__init__.py -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | 4 | # Create your models here. 5 | class Restaurant(models.Model): 6 | name = models.CharField(max_length=50) 7 | street_address = models.CharField(max_length=50) 8 | description = models.CharField(max_length=250) 9 | def __str__(self): 10 | return self.name 11 | 12 | class Review(models.Model): 13 | restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE) 14 | user_name = models.CharField(max_length=20) 15 | rating=models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)]) 16 | review_text = models.CharField(max_length=500) 17 | review_date = models.DateTimeField('review date') 18 | image_name = models.CharField(max_length=100, null=True) 19 | def __str__(self): 20 | return self.restaurant.name + " (" + self.review_date.strftime("%x") +")" 21 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello Azure - Python Django and PostgreSQL Tutorial 4 | 5 | 6 | 7 |
8 |

There was a problem (500 error) with the sample app.

9 |
Try the following troubleshooting tips if running locally: 10 | 19 |
20 |
Try the following troubleshooting tips if running in Azure App Service: 21 | 31 |
32 |
33 | 34 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/restaurant_review/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | 3 | 4 | 5 | {% block head %} 6 | Django web app with PostgreSQL in Azure - {% block title %}{% endblock %} 7 | 8 | 9 | 10 | 11 | {% endblock %} 12 | 13 | 14 | 38 | 39 |
40 | {% block content %}{% endblock %} 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/restaurant_review/create_restaurant.html: -------------------------------------------------------------------------------- 1 | {% extends "restaurant_review/base.html" %} 2 | {% block title %}Restaurant Create{% endblock %} 3 | {% block content %} 4 |

Add New Restaurant

5 | {% if messages %} 6 | 11 | {% endif %} 12 | 13 |
{% csrf_token %} 14 |
15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 | 27 | 28 |
29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/restaurant_review/details.html: -------------------------------------------------------------------------------- 1 | {% extends "restaurant_review/base.html" %} 2 | {% load restaurant_extras %} 3 | {% block title %}Restaurant Details{% endblock %} 4 | {% block content %} 5 |

{{ restaurant.name }}

6 | 7 |
8 |
Street address:
9 |
{{ restaurant.street_address }}
10 |
11 |
12 |
Description:
13 |
{{ restaurant.description }}
14 |
15 |
16 |
Rating:
17 |
{% star_rating restaurant.avg_rating restaurant.review_count %}
18 |
19 | 20 |

Reviews

21 | 22 |

23 | 26 | {% if messages %} 27 |

32 | {% endif %} 33 |

34 | 35 | 36 | {% if restaurant.review_set %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {% for review in restaurant.review_set.all %} 49 | 50 | 51 | 52 | 53 | 54 | 61 | 62 | 63 | {% endfor %} 64 | 65 |
DateUserRatingReviewPhoto
{{ review.review_date }}{{ review.user_name }}{{ review.rating }}{{ review.review_text }} 55 | {% if review.image_name %} 56 | 57 | 58 | 59 | {% endif %} 60 |
66 | {% else %} 67 |

No reviews of this restaurant yet.

68 | {% endif %} 69 | 70 | 71 | 127 | {% endblock %} 128 | 129 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/restaurant_review/index.html: -------------------------------------------------------------------------------- 1 | {% extends "restaurant_review/base.html" %} 2 | {% load restaurant_extras %} 3 | {% block title %}Restaurant List{% endblock %} 4 | {% block content %} 5 |

Restaurants

6 | 7 | {% if restaurants %} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for restaurant in restaurants %} 18 | 19 | 20 | 21 | 22 | 23 | {% endfor %} 24 | 25 |
NameRatingDetails
{{ restaurant.name }}{% star_rating restaurant.avg_rating restaurant.review_count %} Details
26 | {% else %} 27 |

No restaurants exist. Select Add new restaurant to add one.

28 | {% endif %} 29 | 30 |
31 | Add new restaurant 32 |
33 | {% endblock %} -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templates/restaurant_review/star_rating.html: -------------------------------------------------------------------------------- 1 | {% if review_count %} 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | {{ avg_rating|floatformat:1 }} ({{review_count}} {% if review_count == 1 %} review{% else %} reviews{% endif %}) 21 | 22 |
23 | {% else %} 24 | No ratings yet 25 | {% endif %} 26 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/restaurant_review/templatetags/__init__.py -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/templatetags/restaurant_extras.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag('restaurant_review/star_rating.html') 7 | def star_rating(avg_rating, review_count): 8 | stars_percent = round((avg_rating / 5.0) * 100) if review_count > 0 else 0 9 | return {'avg_rating': avg_rating, 'review_count': review_count, 'stars_percent': stars_percent} -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import views 3 | 4 | urlpatterns = [ 5 | path('', views.index, name='index'), 6 | path('/', views.details, name='details'), 7 | path('create', views.create_restaurant, name='create_restaurant'), 8 | path('add', views.add_restaurant, name='add_restaurant'), 9 | path('review/', views.add_review, name='add_review'), 10 | ] -------------------------------------------------------------------------------- /azure_monitor/django_sample/restaurant_review/views.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import os 3 | import azureproject.app_insights 4 | 5 | from django.http import Http404, HttpResponseRedirect 6 | from django.shortcuts import render 7 | from django.db.models import Avg, Count 8 | from django.urls import reverse 9 | from django.utils import timezone 10 | from django.contrib import messages 11 | from azure.identity import DefaultAzureCredential 12 | from azure.storage.blob import BlobServiceClient 13 | from requests import RequestException, exceptions 14 | from azureproject.get_token import get_token 15 | from azureproject.app_insights import * 16 | 17 | from restaurant_review.models import Restaurant, Review 18 | 19 | # Create your views here. 20 | 21 | def index(request): 22 | print('Request for index page received') 23 | get_token() 24 | restaurants = Restaurant.objects.annotate(avg_rating=Avg('review__rating')).annotate(review_count=Count('review')) 25 | return render(request, 'restaurant_review/index.html', {'restaurants': restaurants }) 26 | 27 | 28 | def details(request, id): 29 | print('Request for restaurant details page received') 30 | get_token() 31 | 32 | # Get account_url based on environment 33 | account_url = get_account_url() 34 | image_path = account_url + "/" + os.environ['STORAGE_CONTAINER_NAME'] 35 | 36 | try: 37 | restaurant = Restaurant.objects.annotate(avg_rating=Avg('review__rating')).annotate(review_count=Count('review')).get(pk=id) 38 | except Restaurant.DoesNotExist: 39 | raise Http404("Restaurant doesn't exist") 40 | return render(request, 'restaurant_review/details.html', {'restaurant': restaurant, 41 | 'image_path': image_path}) 42 | 43 | 44 | def create_restaurant(request): 45 | print('Request for add restaurant page received') 46 | 47 | return render(request, 'restaurant_review/create_restaurant.html') 48 | 49 | def add_restaurant(request): 50 | get_token() 51 | try: 52 | name = request.POST['restaurant_name'] 53 | street_address = request.POST['street_address'] 54 | description = request.POST['description'] 55 | if (name == "" or description == ""): 56 | raise RequestException() 57 | except (KeyError, exceptions.RequestException) as e: 58 | # Redisplay the restaurant entry form. 59 | messages.add_message(request, messages.INFO, 'Restaurant not added. Include at least a restaurant name and description.') 60 | return HttpResponseRedirect(reverse('create_restaurant')) 61 | else: 62 | restaurant = Restaurant() 63 | restaurant.name = name 64 | restaurant.street_address = street_address 65 | restaurant.description = description 66 | Restaurant.save(restaurant) 67 | 68 | # log a new metric for a review 69 | tmap = tag_map_module.TagMap() 70 | tmap.insert("name", name) 71 | record_metric_resturant(tmap) 72 | 73 | return HttpResponseRedirect(reverse('details', args=(restaurant.id,))) 74 | 75 | def add_review(request, id): 76 | get_token() 77 | try: 78 | restaurant = Restaurant.objects.annotate(avg_rating=Avg('review__rating')).annotate(review_count=Count('review')).get(pk=id) 79 | except Restaurant.DoesNotExist: 80 | raise Http404("Restaurant doesn't exist") 81 | 82 | try: 83 | user_name = request.POST['user_name'] 84 | rating = request.POST['rating'] 85 | review_text = request.POST['review_text'] 86 | if (user_name == "" or rating == ""): 87 | raise RequestException() 88 | except (KeyError, exceptions.RequestException) as e: 89 | # Redisplay the details page 90 | messages.add_message(request, messages.INFO, 'Review not added. Include at least a name and rating for review.') 91 | return HttpResponseRedirect(reverse('details', args=(id,))) 92 | else: 93 | 94 | if 'reviewImage' in request.FILES: 95 | image_data = request.FILES['reviewImage'] 96 | print("Original image name = " + image_data.name) 97 | print("File size = " + str(image_data.size)) 98 | 99 | if (image_data.size > 2048000): 100 | messages.add_message(request, messages.INFO, 'Image too big, try again.') 101 | return HttpResponseRedirect(reverse('details', args=(id,))) 102 | 103 | # Get account_url based on environment 104 | account_url = get_account_url() 105 | 106 | # Create client 107 | azure_credential = DefaultAzureCredential(exclude_shared_token_cache_credential=True) 108 | blob_service_client = BlobServiceClient( 109 | account_url=account_url, 110 | credential=azure_credential) 111 | 112 | # Get file name to use in database 113 | image_name = str(uuid.uuid4()) + ".png" 114 | 115 | # Create blob client 116 | blob_client = blob_service_client.get_blob_client(container=os.environ['STORAGE_CONTAINER_NAME'], blob=image_name) 117 | print("\nUploading to Azure Storage as blob:\n\t" + image_name) 118 | 119 | # Upload file 120 | with image_data as data: 121 | blob_client.upload_blob(data) 122 | else: 123 | # No image for review 124 | image_name=None 125 | 126 | review = Review() 127 | review.restaurant = restaurant 128 | review.review_date = timezone.now() 129 | review.user_name = user_name 130 | review.rating = rating 131 | review.review_text = review_text 132 | review.image_name = image_name 133 | Review.save(review) 134 | 135 | # log a new metric for a review 136 | tmap = tag_map_module.TagMap() 137 | tmap.insert("resturantId", str(id)) 138 | record_metric_review(tmap) 139 | 140 | return HttpResponseRedirect(reverse('details', args=(id,))) 141 | 142 | def get_account_url(): 143 | # Create LOCAL_USE_AZURE_STORAGE environment variable to use Azure Storage locally. 144 | if 'WEBSITE_HOSTNAME' in os.environ or ("LOCAL_USE_AZURE_STORAGE" in os.environ): 145 | return "https://%s.blob.core.windows.net" % os.environ['STORAGE_ACCOUNT_NAME'] 146 | else: 147 | return os.environ['STORAGE_ACCOUNT_NAME'] 148 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/static/favicon.ico -------------------------------------------------------------------------------- /azure_monitor/django_sample/static/images/Example-reviews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/static/images/Example-reviews.png -------------------------------------------------------------------------------- /azure_monitor/django_sample/static/images/azure-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/static/restaurant.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 75rem; 3 | padding-top: 4.5rem; 4 | } 5 | .score { 6 | display: block; 7 | font-size: 16px; 8 | position: relative; 9 | overflow: hidden; 10 | } 11 | 12 | .score-wrap { 13 | display: inline-block; 14 | position: relative; 15 | height: 19px; 16 | } 17 | 18 | .score .stars-active { 19 | color: #EEBD01; 20 | position: relative; 21 | z-index: 10; 22 | display: inline-block; 23 | overflow: hidden; 24 | white-space: nowrap; 25 | } 26 | 27 | .score .stars-inactive { 28 | color: grey; 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | -webkit-text-stroke: initial; 33 | } 34 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/staticfiles/note.txt: -------------------------------------------------------------------------------- 1 | Here for https://github.com/evansd/whitenoise/issues/215. -------------------------------------------------------------------------------- /azure_monitor/django_sample/web_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/django_sample/web_project/__init__.py -------------------------------------------------------------------------------- /azure_monitor/django_sample/web_project/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for web_project project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_project.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/web_project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for web_project project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.0/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-385jn!oj!%2yz67zpz*h$sb+8x41^i#)zdo+qo$=pd28^4-5-r' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'web_project.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'web_project.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/4.0/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': BASE_DIR / 'db.sqlite3', 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/4.0/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_TZ = True 113 | 114 | 115 | # Static files (CSS, JavaScript, Images) 116 | # https://docs.djangoproject.com/en/4.0/howto/static-files/ 117 | 118 | STATIC_URL = 'static/' 119 | 120 | # Default primary key field type 121 | # https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field 122 | 123 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 124 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/web_project/urls.py: -------------------------------------------------------------------------------- 1 | """web_project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/4.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /azure_monitor/django_sample/web_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for web_project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'web_project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /azure_monitor/env.template: -------------------------------------------------------------------------------- 1 | APPINSIGHTS_INSTRUMENTATIONKEY={INSIGHTS_KEY} 2 | APPLICATIONINSIGHTS_CONNECTION_STRING={INSIGHTS_CONNECTION_STRING} 3 | 4 | DB_SERVER=python-appinsights-{SUFFIX}.database.windows.net 5 | DB_DATABASE=python 6 | DB_USERNAME={DBUSER} 7 | DB_PASSWORD={DBPASSWORD} 8 | DB_PORT = 1433 9 | 10 | DBHOST=python-appinsights-{SUFFIX}-mysql.mysql.database.azure.com 11 | DBNAME=python 12 | DBUSER={DBUSER}@python-appinsights-{SUFFIX}-mysql 13 | DBPASS={DBPASSWORD} 14 | 15 | AZURE_STORAGE_CONNECTION_STRING={STORAGE_CONNECTION_STRING} 16 | FUNCTION_URL={FUNCTION_URL} -------------------------------------------------------------------------------- /azure_monitor/flask_sample/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - python 5 | - html 6 | products: 7 | - azure 8 | description: "This sample contains a simple Flask application to show how you can instrument the OpenCensus Azure Monitor exporters as well as track telemetry from popular Python libraries via OpenCensus integrations." 9 | urlFragment: azure-monitor-opencensus-python 10 | --- 11 | 12 | # Flask "To-Do" Sample Application 13 | 14 | ## Setup 15 | 16 | 1. This package is not hosted on Pypi. You can install all dependencies locally using `pip install -r requirements.txt`. 17 | 2. To send telemetry to Azure Monitor, pass in your instrumentation key into `INSTRUMENTATION_KEY` in `config.py`. 18 | 19 | ``` 20 | INSTRUMENTATION_KEY = 21 | ``` 22 | 23 | The default database URI links to a sqlite database `app.db`. To configure a different database, you can modify `config.py` and change the `SQLALCHEMY_DATABASE_URI` value to point to a database of your choosing. 24 | 25 | ``` 26 | SQLALCHEMY_DATABASE_URI = 27 | ``` 28 | 29 | ## Usage 30 | 31 | 1. Navigate to where `azure_monitor\flask_sample` is located. 32 | 2. Run the main application via `python sample.py`. 33 | 4. Hit your local endpoint (should be http://localhost:5000). This should open up a browser to the main page. 34 | 5. On the newly opened page, you can add tasks via the textbox under `Add a new todo item:`. You can enter any text you want (cannot be blank). 35 | 6. Click `Add Item` to add the task. The task will be added under `Incomplete Items`. Adding an item with greater than 10 characters will generate an error. 36 | 7. To utilize the `Save to File` feature, run the endpoint application via `python endpoint.py`. This will run another Flask application with a WSGI server running on http://localhost:5001. Click `Save to File` and all tasks will be written to a file `file.txt` in the `output` folder. 37 | 8. Each task has a `Mark As Complete` button. Clicking it will move the task from incomplete to completed. 38 | 9. You can also hit the `blacklist` url page to see a sample of a page that does not have telemetry being sent (http://localhost:5000/blacklist). 39 | 10. You can also run a command line interface application to hit the endpoints on you Flask application directly. Run `command.py` and follow the prompts accordingly. 40 | 41 | ## Types of telemetry sent 42 | 43 | There are various types of telemetry that are being sent in the sample application. Refer to [Telemetry Type in Azure Monitor](https://docs.microsoft.com/azure/azure-monitor/app/opencensus-python#telemetry-type-mappings). Every button click hits an endpoint that exists in the flask application, so they will be treated as incoming requests (`requests` table in Azure Monitor). A log message is also sent every time a button is clicked, so a log telemetry is sent (`traces` table in Azure Monitor). An exception telemetry is sent when an invalid task is entered (greater than 10 characters). A counter metric is recorded every time the `add` button is clicked. Metric telemetry is sent every interval (default 15.0 s, `customMetrics` table in Azure Monitor). 44 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/flask_sample/app.db -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | from flask import Flask 5 | 6 | sys.path.append('..') 7 | from config import Config 8 | from flask_sqlalchemy import SQLAlchemy 9 | from opencensus.ext.azure import metrics_exporter 10 | from opencensus.ext.azure.log_exporter import AzureLogHandler 11 | from opencensus.ext.flask.flask_middleware import FlaskMiddleware 12 | from opencensus.trace import config_integration 13 | 14 | logger = logging.getLogger(__name__) 15 | app = Flask(__name__) 16 | app.config.from_object(Config) 17 | 18 | db = SQLAlchemy(app) 19 | 20 | # Import here to avoid circular imports 21 | from app import routes # noqa isort:skip 22 | 23 | # Trace integrations for sqlalchemy library 24 | config_integration.trace_integrations(['sqlalchemy']) 25 | 26 | # Trace integrations for requests library 27 | config_integration.trace_integrations(['requests']) 28 | 29 | # FlaskMiddleware will track requests for the Flask application and send 30 | # request/dependency telemetry to Azure Monitor 31 | middleware = FlaskMiddleware(app) 32 | 33 | # Processor function for changing the role name of the app 34 | def callback_function(envelope): 35 | envelope.tags['ai.cloud.role'] = "To-Do App" 36 | return True 37 | 38 | # Adds the telemetry processor to the trace exporter 39 | middleware.exporter.add_telemetry_processor(callback_function) 40 | 41 | # Exporter for metrics, will send metrics data 42 | exporter = metrics_exporter.new_metrics_exporter( 43 | enable_standard_metrics=False, 44 | connection_string='InstrumentationKey=' + Config.INSTRUMENTATION_KEY) 45 | 46 | # Exporter for logs, will send logging data 47 | logger.addHandler( 48 | AzureLogHandler( 49 | connection_string='InstrumentationKey=' + Config.INSTRUMENTATION_KEY 50 | ) 51 | ) 52 | 53 | 54 | if __name__ == '__main__': 55 | app.run(host='localhost', port=5000, threaded=True, debug=True) 56 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, SubmitField 3 | from wtforms.validators import DataRequired 4 | 5 | 6 | class ToDoForm(FlaskForm): 7 | add_input = StringField('To Do', validators=[DataRequired()]) 8 | mark_submit = SubmitField('Mark As Complete') 9 | valid_submit = SubmitField('Save to File') 10 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/metrics.py: -------------------------------------------------------------------------------- 1 | from opencensus.stats import aggregation as aggregation_module 2 | from opencensus.stats import measure as measure_module 3 | from opencensus.stats import stats as stats_module 4 | from opencensus.stats import view as view_module 5 | from opencensus.tags import tag_map as tag_map_module 6 | 7 | stats = stats_module.stats 8 | view_manager = stats.view_manager 9 | stats_recorder = stats.stats_recorder 10 | 11 | request_measure = measure_module.MeasureInt("added tasks", 12 | "number of added tasks", 13 | "tasks") 14 | request_view = view_module.View("added tasks", 15 | "number of tasks", 16 | ["application_type"], 17 | request_measure, 18 | aggregation_module.CountAggregation()) 19 | view_manager.register_view(request_view) 20 | mmap = stats_recorder.new_measurement_map() 21 | tmap = tag_map_module.TagMap() 22 | tmap.insert("application_type", "flask") 23 | tmap.insert("os_type", "linux") 24 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/models/__init__.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | 3 | 4 | class Todo(db.Model): 5 | id = db.Column(db.Integer, primary_key=True) 6 | text = db.Column(db.String(200)) 7 | complete = db.Column(db.Boolean) 8 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/routes.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | from flask import flash, make_response, redirect, render_template, request, url_for 5 | 6 | from app import app, db, logger 7 | from app.forms import ToDoForm 8 | from app.metrics import mmap, request_measure, tmap 9 | from app.models import Todo 10 | 11 | # Hitting any endpoint will track an incoming request (requests) 12 | 13 | 14 | @app.route('/') 15 | @app.route('/error') 16 | def index(): 17 | form = ToDoForm() 18 | # Queries to the data base will track an outgoing request (dependencies) 19 | incomplete = Todo.query.filter_by(complete=False).all() 20 | complete = Todo.query.filter_by(complete=True).all() 21 | path = request.url_rule 22 | if path and 'error' in path.rule: 23 | flash('ERROR: String must be less than 11 characters.') 24 | return render_template( 25 | 'index.html', 26 | title='Home', 27 | form=form, 28 | complete=complete, 29 | incomplete=incomplete 30 | ) 31 | 32 | 33 | @app.route('/save', methods=['POST']) 34 | def save(): 35 | incomplete = Todo.query.filter_by(complete=False).all() 36 | complete = Todo.query.filter_by(complete=True).all() 37 | incomplete.extend(complete) 38 | url = "http://localhost:5001/api/save" 39 | entries = ["Id: " + str(entry.id) + " Task: " + entry.text + " Complete: " + str(entry.complete) \ 40 | for entry in incomplete] 41 | response = requests.post(url=url, data=json.dumps(entries)) 42 | if response.ok: 43 | flash("Todo saved to file.") 44 | else: 45 | logger.error(response.reason) 46 | flash("Exception occurred while saving") 47 | return redirect('/') 48 | 49 | 50 | @app.route('/blacklist') 51 | def blacklist(): 52 | return render_template('blacklist.html') 53 | 54 | 55 | @app.route('/add', methods=['POST']) 56 | def add(): 57 | add_input = request.form['add_input'] 58 | # Fail if string greater than 10 characters 59 | try: 60 | if len(add_input) > 10: 61 | raise Exception 62 | todo = Todo(text=add_input, complete=False) 63 | db.session.add(todo) 64 | db.session.commit() 65 | # Logging with the logger will be tracked as logging telemetry (traces) 66 | logger.warn("Added entry: " + todo.text) 67 | # Records a measure metric to be sent as telemetry (customMetrics) 68 | mmap.measure_int_put(request_measure, 1) 69 | mmap.record(tmap) 70 | except Exception: 71 | logger.exception("ERROR: Input length too long.") 72 | return redirect('/error') 73 | return redirect('/') 74 | 75 | 76 | @app.route('/complete/', methods=['POST']) 77 | def complete(id): 78 | todo = Todo.query.filter_by(id=int(id)).first() 79 | todo.complete = True 80 | db.session.commit() 81 | logger.warn("Marked complete: " + todo.text) 82 | return redirect('/') 83 | 84 | ### Endpoints for CLI demo ### 85 | 86 | 87 | @app.route('/get/incomplete') 88 | def get_incomplete(): 89 | incomplete = Todo.query.filter_by(complete=False).all() 90 | return json.dumps([(task.id, task.text) for task in incomplete]) 91 | 92 | 93 | @app.route('/get/complete') 94 | def get_complete(): 95 | complete = Todo.query.filter_by(complete=True).all() 96 | return json.dumps([(task.id, task.text) for task in complete]) 97 | 98 | 99 | @app.route('/add/', methods=['POST']) 100 | def add_task(task): 101 | try: 102 | print("Task Received: " + task) 103 | if len(task) > 10: 104 | raise Exception 105 | todo = Todo(text=task, complete=False) 106 | db.session.add(todo) 107 | db.session.commit() 108 | # Logging with the logger will be tracked as logging telemetry (traces) 109 | logger.warn("Added entry: " + todo.text) 110 | # Records a measure metric to be sent as telemetry (customMetrics) 111 | mmap.measure_int_put(request_measure, 1) 112 | mmap.record(tmap) 113 | except Exception: 114 | logger.exception("ERROR: Input length too long.") 115 | return make_response("ERROR: Input length too long.", 500) 116 | return make_response("Successfully added task.", 200) 117 | 118 | 119 | @app.route('/complete/task/', methods=['POST']) 120 | def complete_task(id): 121 | todo = Todo.query.filter_by(id=int(id)).first() 122 | todo.complete = True 123 | db.session.commit() 124 | logger.warn("Marked complete: " + todo.text) 125 | return make_response("Success", 200) 126 | 127 | 128 | @app.route('/save/tasks', methods=['POST']) 129 | def save_tasks(): 130 | incomplete = Todo.query.filter_by(complete=False).all() 131 | complete = Todo.query.filter_by(complete=True).all() 132 | incomplete.extend(complete) 133 | url = "http://localhost:5001/api/save" 134 | entries = ["Id: " + str(entry.id) + " Task: " + entry.text + " Complete: " + str(entry.complete) \ 135 | for entry in incomplete] 136 | response = requests.post(url=url, data=json.dumps(entries)) 137 | if response.ok: 138 | logger.warn("Todo saved to file.") 139 | return make_response("Todo saved to file.", 200) 140 | else: 141 | logger.error(response.reason) 142 | return make_response("Exception occurred while saving.", 500) 143 | return redirect('/') -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/static/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | line-height: 1.4em; 4 | background: #f5f5f5; 5 | color: #111111; 6 | min-width: 230px; 7 | max-width: 550px; 8 | margin: 0 auto; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | font-weight: 300; 12 | } 13 | 14 | .todoapp { 15 | background: #fff; 16 | margin: 130px 0 40px 0; 17 | position: relative; 18 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 19 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 20 | } 21 | 22 | .header { 23 | background: #f5f5f5; 24 | position: relative; 25 | } 26 | 27 | .todoapp h1 { 28 | position: absolute; 29 | top: -140px; 30 | width: 100%; 31 | font-size: 80px; 32 | font-weight: 200; 33 | text-align: center; 34 | color: #b83f45; 35 | text-rendering: optimizeLegibility; 36 | } 37 | 38 | .header-container div { 39 | display: inline-block; 40 | height: 100%; 41 | } 42 | 43 | /* Save Button */ 44 | .save-button { 45 | box-shadow:inset 0px 39px 0px -24px #e67a73; 46 | background-color:#e4685d; 47 | border-radius:4px; 48 | border:1px solid #ffffff; 49 | display:inline-block; 50 | cursor:pointer; 51 | color:#ffffff; 52 | font-family:Arial; 53 | font-size:15px; 54 | padding:6px 15px; 55 | text-decoration:none; 56 | text-shadow:0px 1px 0px #b23e35; 57 | } 58 | .save-button:hover { 59 | background-color:#eb675e; 60 | } 61 | .save-button:active { 62 | position:relative; 63 | top:1px; 64 | } 65 | 66 | /* Entry section */ 67 | 68 | .new-todo, 69 | .edit { 70 | position: relative; 71 | margin: 0; 72 | width: 100%; 73 | font-size: 24px; 74 | font-family: inherit; 75 | font-weight: inherit; 76 | line-height: 1.4em; 77 | color: inherit; 78 | padding: 6px; 79 | border: 1px solid #999; 80 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 81 | box-sizing: border-box; 82 | -webkit-font-smoothing: antialiased; 83 | -moz-osx-font-smoothing: grayscale; 84 | } 85 | 86 | .new-todo { 87 | padding: 16px 16px 16px 60px; 88 | border: none; 89 | background: rgba(0, 0, 0, 0.003); 90 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 91 | } 92 | 93 | .main { 94 | position: relative; 95 | z-index: 2; 96 | border-top: 1px solid #e6e6e6; 97 | } 98 | 99 | .todo-list { 100 | margin: 0; 101 | padding: 0; 102 | list-style: none; 103 | } 104 | 105 | .todo-list li { 106 | position: relative; 107 | font-size: 24px; 108 | border-bottom: 1px solid #ededed; 109 | } 110 | 111 | .todo-list li:last-child { 112 | border-bottom: none; 113 | } 114 | 115 | .list-element { 116 | width: 100%; 117 | text-align: left; 118 | font-size: 20px; 119 | } 120 | 121 | .footer { 122 | padding: 10px 15px; 123 | height: 20px; 124 | text-align: center; 125 | font-size: 15px; 126 | border-top: 1px solid #e6e6e6; 127 | } 128 | 129 | .footer:before { 130 | content: ''; 131 | position: absolute; 132 | right: 0; 133 | bottom: 0; 134 | left: 0; 135 | height: 50px; 136 | overflow: hidden; 137 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 138 | 0 8px 0 -3px #f6f6f6, 139 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 140 | 0 16px 0 -6px #f6f6f6, 141 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 142 | } -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/templates/blacklist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo App 6 | 7 | 8 | 9 |

The url for this page is blacklisted so no tracing telemetry should be sent when hitting this page.

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Todo App 6 | 7 | 8 | 9 |
10 |
11 |

Todo List

12 |
13 |
14 |
15 | {{ form.valid_submit(class_="save-button") }} 16 |
17 |
18 |
19 | {% with messages = get_flashed_messages() %} 20 | {% if messages %} 21 |
    22 | {% for message in messages %} 23 |

    {{ message }}

    24 | {% endfor %} 25 |
26 | {% endif %} 27 | {% endwith %} 28 |
29 |
30 |
31 |
32 |
33 | {{ form.add_input(class_="new-todo", placeholder="Enter a new task..." ) }} 34 |
35 |
36 |
37 |

Incomplete Items

38 |
    39 | {% for todo in incomplete %} 40 |
  • 41 | 42 |
    43 | 44 | 45 |
    46 |
  • 47 | {% endfor %} 48 |
49 |

Completed Items

50 |
    51 | {% for todo in complete %} 52 |
  • 53 | 54 |
  • 55 | {% endfor %} 56 |
57 |
58 | 59 |
60 |
61 | 62 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/command.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import json 3 | import requests 4 | 5 | from cmd import Cmd 6 | 7 | logger = logging.getLogger(__name__) 8 | class MyPrompt(Cmd): 9 | intro = "Python Demo To-Do Application. Type ? to list commands" 10 | 11 | def do_exit(self, input): 12 | '''exit the application.''' 13 | print("Bye") 14 | return True 15 | 16 | def do_show(self, input): 17 | incomplete = [] 18 | complete = [] 19 | try: 20 | incomplete = requests.get("http://localhost:5000/get/incomplete") 21 | complete = requests.get("http://localhost:5000/get/complete") 22 | inc_data = json.loads(incomplete.text) 23 | com_data = json.loads(complete.text) 24 | print("Incomplete\n") 25 | print("----------\n") 26 | for entry in inc_data: 27 | print("Id: " + str(entry[0]) + " Task: " + entry[1] + "\n") 28 | print("Complete\n") 29 | print("----------\n") 30 | for entry in com_data: 31 | print("Id: " + str(entry[0]) + " Task: " + entry[1] + "\n") 32 | except Exception as ex: 33 | logger.exception("Error occured.") 34 | pass 35 | 36 | def do_add(self, text): 37 | try: 38 | response = requests.post("http://localhost:5000/add/" + text) 39 | print(response.text) 40 | if response.ok: 41 | self.do_show(None) 42 | except Exception as ex: 43 | logger.exception("Error occured.") 44 | pass 45 | 46 | def do_complete(self, id): 47 | try: 48 | response = requests.post("http://localhost:5000/complete/task/" + str(id)) 49 | print(response.text) 50 | if response.ok: 51 | self.do_show(None) 52 | except Exception as ex: 53 | logger.exception("Error occured.") 54 | pass 55 | 56 | def do_save(self, input): 57 | try: 58 | response = requests.post("http://localhost:5000/save/tasks") 59 | print(response.text) 60 | if response.ok: 61 | self.do_show(None) 62 | except Exception as ex: 63 | logger.exception("Error occured.") 64 | pass 65 | 66 | MyPrompt().cmdloop() -------------------------------------------------------------------------------- /azure_monitor/flask_sample/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | basedir = os.path.abspath(os.path.dirname(__file__)) 4 | 5 | 6 | class Config(object): 7 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'secret-key' 8 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 9 | 'sqlite:///' + os.path.join(basedir, 'app.db') 10 | SQLALCHEMY_TRACK_MODIFICATIONS = False 11 | INSTRUMENTATION_KEY = os.environ.get('APPINSIGHTS_INSTRUMENTATIONKEY') or \ 12 | '' 13 | CONNECTION_STRING = 'InstrumentationKey=' + INSTRUMENTATION_KEY 14 | sampler = 'opencensus.trace.samplers.ProbabilitySampler(rate=1.0)' 15 | OPENCENSUS = { 16 | 'TRACE': { 17 | 'SAMPLER': sampler, 18 | 'EXPORTER': 'opencensus.ext.azure.trace_exporter.AzureExporter(connection_string="' 19 | + CONNECTION_STRING + '")', 20 | 'BLACKLIST_PATHS': ['blacklist'], 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/endpoint.py: -------------------------------------------------------------------------------- 1 | from endpoint import endpoint_app 2 | 3 | endpoint_app.run(host='localhost', port=5001, threaded=True, debug=True) 4 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/endpoint/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from flask import Flask 3 | 4 | sys.path.append('..') 5 | endpoint_app = Flask(__name__) 6 | 7 | # Import here to avoid circular imports 8 | from endpoint import endpoint_routes # noqa isort:skip 9 | 10 | 11 | if __name__ == '__main__': 12 | endpoint_app.run(host='localhost', port=5001, threaded=True, debug=True) 13 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/endpoint/endpoint_routes.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from endpoint import endpoint_app 5 | from flask import make_response, request 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | @endpoint_app.route('/api/save', methods=['POST']) 11 | def save_tasks(): 12 | try: 13 | data = json.loads(request.data) 14 | with open('./output/file.txt', 'w') as file: 15 | for item in data: 16 | file.write(item) 17 | file.write('\n') 18 | except Exception as ex: 19 | logger.exception("Exception occurred while saving: ") 20 | return make_response("Server exception: " + ex, 500) 21 | return request.data 22 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/output/file.txt: -------------------------------------------------------------------------------- 1 | Id: 1 Task: Test Complete: False 2 | Id: 2 Task: asd Complete: False 3 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/requirements.txt: -------------------------------------------------------------------------------- 1 | flask-sqlalchemy >= 2.4.1 2 | flask-wtf >= 0.14.3 3 | opencensus-ext-azure >= 1.0.2 4 | opencensus-ext-flask >= 0.7.3 5 | opencensus-ext-requests >= 0.7.3 6 | opencensus-ext-sqlalchemy >= 0.1.2 7 | -------------------------------------------------------------------------------- /azure_monitor/flask_sample/sample.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | app.run(host='localhost', port=5000, threaded=True, debug=True) 4 | -------------------------------------------------------------------------------- /azure_monitor/media/python_azure_dependencies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_azure_dependencies.png -------------------------------------------------------------------------------- /azure_monitor/media/python_azure_resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_azure_resources.png -------------------------------------------------------------------------------- /azure_monitor/media/python_custommetrics-carrots-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_custommetrics-carrots-logs.png -------------------------------------------------------------------------------- /azure_monitor/media/python_custommetrics-carrots.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_custommetrics-carrots.png -------------------------------------------------------------------------------- /azure_monitor/media/python_custommetrics-web-reviews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_custommetrics-web-reviews.png -------------------------------------------------------------------------------- /azure_monitor/media/python_functionapp_deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_functionapp_deploy.png -------------------------------------------------------------------------------- /azure_monitor/media/python_simple_app_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_simple_app_trace.png -------------------------------------------------------------------------------- /azure_monitor/media/python_simple_exception_custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_simple_exception_custom.png -------------------------------------------------------------------------------- /azure_monitor/media/python_simple_trace_trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_simple_trace_trace.png -------------------------------------------------------------------------------- /azure_monitor/media/python_webapp_requests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/media/python_webapp_requests.png -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This document covers the python logger class using [OpenCensus-Python sdk][1] for [Azure Application Insights][3]. 4 | 5 | ## Application Monitoring 6 | 7 | Application monitoring is one of the important pillars for any software development project. To support monitoring and observability, software projects should be able to perform below tasks: 8 | 9 | * Ability to generate logs and send them to analytics workspace. 10 | * Ability for monitor exceptions. 11 | * Ability to send custom metrics or events from the application. 12 | 13 | Currently application monitoring for python projects is integrated with [Azure Application Insights][3] using [opencensus-python sdk][1]. 14 | 15 | ## Why there is a need for Python Logger Class? 16 | 17 | Currently [Azure Application Insights][3] supports logging, distributed tracing, metric collection using [opencensus-python sdk][1]. To integrate any python project with `opencensus sdk`, user has to write a lot of bootstrap code and this needs to be repeated for all the python projects in the application. 18 | By implementing Python Logger class, all the bootstrap logic can be encapsulated inside a single class which then can be reused by multiple projects. 19 | 20 | For example, here is the link to Azure documentation for [opencensus-python sdk][1] and to add a very basic logger and tracer to a flask application following code is required: 21 | 22 | ```python 23 | 24 | import logging 25 | from opencensus.ext.azure.log_exporter import AzureLogHandler 26 | from opencensus.trace.samplers import ProbabilitySampler 27 | from opencensus.ext.azure.trace_exporter import AzureExporter 28 | from opencensus.trace.tracer import Tracer 29 | from opencensus.trace import config_integration 30 | from opencensus.ext.flask.flask_middleware import FlaskMiddleware 31 | 32 | APP_NAME = "Flask_App" 33 | APP = Flask(APP_NAME) 34 | 35 | config_integration.trace_integrations(["requests"]) 36 | config_integration.trace_integrations(["logging"]) 37 | 38 | def callback_add_role_name(envelope): 39 | """ Callback function for opencensus """ 40 | envelope.tags["ai.cloud.role"] = APP_NAME 41 | return True 42 | 43 | app_insights_cs = "InstrumentationKey=" + APP_INSIGHTS_KEY 44 | logger = logging.getLogger(__name__) 45 | handler = AzureLogHandler(connection_string=app_insights_cs) 46 | handler.add_telemetry_processor(callback_add_role_name) 47 | logger.setLevel(logging.INFO) 48 | logger.addHandler(handler) 49 | 50 | azure_exporter = AzureExporter(connection_string=app_insights_cs) 51 | azure_exporter.add_telemetry_processor(callback_add_role_name) 52 | 53 | FlaskMiddleware( 54 | APP, exporter=azure_exporter, sampler=ProbabilitySampler(rate=1.0), 55 | ) 56 | 57 | tracer = Tracer(exporter=azure_exporter, sampler=ProbabilitySampler(1.0)) 58 | 59 | ``` 60 | 61 | This is the bare minimum code required for creating logger and tracer. However, to make it production ready, more complex logic is required. 62 | Following are some of the scenarios which are not covered in basic implementation. 63 | 64 | * Set logging config values from a json file. 65 | * Read application insights key from environment variable or configuration passed during initialization of loggers. 66 | * Add support for various integrations like flask, sql, httplib, django, pymysql etc 67 | * Adding default dimensions to the logger so that those dimensions get logged with every logging statement. 68 | * Adding [cloud role name][4]. This is required to give the name of component which is required to identify components in [Application map][4]. 69 | * Add disabled logger which can be used during Unit testing. 70 | 71 | Assuming, if we need to add code for above scenarios, then this will get repeated for every python project in our application. To avoid repetition and hence enabling code reuse, this python logger class is required. 72 | 73 | ## Prerequisites for using AppLogger 74 | 75 | * An Azure subscription 76 | * Create an [Application Insights resource](https://docs.microsoft.com/azure/azure-monitor/app/create-new-resource) and store the application insights instrumentation key 77 | which is used in python logger class. 78 | * Using one of the supported [Python versions](https://github.com/census-instrumentation/opencensus-python/blob/master/setup.py#L25) 79 | 80 | ## Design 81 | 82 | ### AppLogger 83 | 84 | AppLogger is the logging class which contains bootstrap code to initialize logger using opencensus python sdk. 85 | 86 | ```python 87 | class AppLogger: 88 | def __init__(self, config=None): 89 | """Create an instance of the Logger class. 90 | 91 | Args: 92 | config:([dict], optional): 93 | Contains the setting for logger {"log_level": "DEBUG","logging_enabled":"true"", 94 | "app_insights_key":""} 95 | """ 96 | ... 97 | 98 | def get_logger(self, component_name="AppLogger", custom_dimensions={}): 99 | ... 100 | def get_tracer(self, component_name="AppLogger", parent_tracer=None): 101 | ... 102 | def get_event_logger(self, component_name="AppLogger", custom_dimensions={}): 103 | ... 104 | 105 | ``` 106 | 107 | Parameters used in initialization AppLogger: 108 | 109 | * **config**:(optional): This is an optional parameter. Its a python `dict` which takes config values required for logger. 110 | 111 | ```json 112 | config = { 113 | "log_level": "DEBUG", 114 | "logging_enabled": "true", 115 | "app_insights_key": "", 116 | } 117 | ``` 118 | 119 | 1. **log_level**(optional): Log level can be set by passing desired value in config dict. 120 | Following log levels are supported 121 | * "INFO" 122 | * "DEBUG" 123 | * "WARN" 124 | * "ERROR" 125 | * "CRITICAL" 126 | 127 | > Default value for `log_level` is set to "INFO" 128 | 129 | 2. **logging_enabled**(optional): This is used to enable or disable the logging. By default its value is set to "true". To disable logging `logging_enabled` can be set to `false`. This is useful when we need to run unit tests and don't want to send telemetry. Following could be scenarios where logging can be turned off: 130 | 1. Unit Tests 131 | 1. Local development 132 | 133 | > Please make sure to set application insights key even when logging_enabled is set to "false" otherwise, it will throw exception during creation of logger. 134 | 135 | 3. **app_insights_key**: This contains the value of application insights key. If **not set** here, it should be set as an environment variable as `APPINSIGHTS_INSTRUMENTATION_KEY="YOUR KEY"`. 136 | 137 | > If application insights key is neither set in config, nor in environment variable, initialization of AppLogger will fail with exception. 138 | 139 | * **Function `get_logger`** 140 | 141 | ```python 142 | def get_logger(self, component_name="AppLogger", custom_dimensions={}): 143 | ``` 144 | 145 | `get_logger` function adds AzureLogHandler to the logger and also adds `ai.cloud.role`= [component_name][6] to the logger. It also adds the default dimensions and returns a logger object. 146 | 147 | * **Function `get_tracer`** 148 | 149 | ```python 150 | def get_tracer(self, component_name="AppLogger", parent_tracer=None): 151 | ``` 152 | 153 | `get_tracer` function return a Tracer object. It sets the [component_name][6] to `ai.cloud.role` which is used to identify component in application map. It also sets parent_tracer for proper correlation. 154 | 155 | * **Function `get_event_logger`** 156 | 157 | ```python 158 | def get_event_logger(self, component_name="AppLogger", custom_dimensions={}): 159 | ``` 160 | 161 | `get_event_logger` function adds AzureEventHandler to the logger and also adds `ai.cloud.role`= [component_name][6] to the logger. It also adds the default dimensions and returns a logger object. 162 | 163 | 164 | * **Function `enable_flask`** 165 | 166 | ```python 167 | def enable_flask(self,flask_app,component_name="AppLogger"): 168 | ``` 169 | 170 | `enable_flask` function enables Flask middleware as mentioned in the [documentation.][5] This is required to enable tracing for flask applications. 171 | 172 | Currently function for enabling flask is added but if application requires [integration][7] with other libraries like httplib, django, mysql, pymysql, pymongo, fastapi, postgresql etc then corresponding functions need to be added. 173 | 174 | * **Parameters used in `get_logger`, `get_tracer` and `get_event_logger`functions in AppLogger**: 175 | 176 | * **component_name**: (optional): Default value of this param is "AppLogger". Its always best practice to pass this parameter as the name of component in which logger is initialized eg "API3" etc. This will be help in filtering logs in application insights based on the `component_name`. This will appear as `cloud_RoleName` in app insight logs. Here is screenshot of application map in application insights: 177 | ![alt text](./monitoring/img/application_map.png) 178 | 179 | * **parent_tracer**: 180 | This is required to set the correlation for tracing. Ideally this is required to set correlation of logs for modules which are called within same process. 181 | Lets takes an example where we have `main.py` which calls a function in `package1`. Now, to correlate logs generated from main.py and package1, a `tracer` is created in `main.py` which is passed to `package1_function` as `parent_tracer`. In `package1_function` a tracer is created where parent `traceid` is set in `get_tracer` function. 182 | 183 | By correlating traceids, all the logs can be filtered with one traceid. 184 | 185 | > Please note that in Azure Application Insights, `traceid` is referred as `operation_id`. 186 | 187 | ```py 188 | # main.py 189 | config={"app_insights_key"=""} 190 | app_logger= AppLogger(config) 191 | tracer= app_logger.get_tracer(component_name="main") 192 | # call package1 function 193 | some_result = package1_function(app_logger=app_logger,parent_tracer= tracer) 194 | 195 | # package1 196 | config={"app_insights_key"=""} 197 | def package1_function(app_logger, parent_tracer=None): 198 | app_logger = AppLogger(config) 199 | tracer = app_logger.get_tracer(component_name="package1",parent_tracer) 200 | ``` 201 | 202 | Here is the screenshot of application insights showing correlation of traceids. 203 | 204 | ![alt text](./monitoring/img/operation_id.png) 205 | 206 | * **custom_dimensions**: Custom dimensions is a dict containing 'key:value' pairs for setting default custom dimension for logger. 207 | 208 | ```py 209 | custom_dimenions = { 210 | "key1":"value1", 211 | "key2":"value2" 212 | } 213 | app_logger= AppLogger(config) 214 | logger = app_logger.get_logger(self, component_name="Package1", custom_dimensions=custom_dimensions): 215 | 216 | def my_method(self, **kwargs): 217 | # Do something 218 | # Then log 219 | logger.warning("Some warning", extra=MyParam) 220 | logger.info("Some info",extra=MyParam) 221 | ``` 222 | 223 | ### Create spans and dependency tracking 224 | 225 | To track and log time of any function, tracer span can be used as shown in following example: 226 | 227 | ```py 228 | config = { 229 | "log_level": "DEBUG", 230 | "logging_enabled": "true", 231 | "app_insights_key": "", 232 | } 233 | app_logger = AppLogger(config) 234 | logger = app_logger.get_logger(component_name="SomeModule") 235 | tracer = app_logger.get_tracer(component_name="SomeModule") 236 | with tracer.span("testspan"): 237 | test_function(app_logger) 238 | 239 | def test_function(app_logger=get_disabled_logger()): 240 | pass 241 | ``` 242 | 243 | Above code generates entry in dependency table in application insights for `target` = `testspan` with time duration taken by `test_function`. For example, in below screenshot we can see that time taken by `util_func` is around 19 sec. 244 | 245 | ![alt text](./monitoring/img/dependency.png) 246 | 247 | More info about dependency monitoring can be found [here.][7] 248 | 249 | ### Unit testing 250 | 251 | Unit tests for application using `AppLogger` can use either `logging_enabled` = `false` or `get_disabled_logger()`. This will disable logging during unit tests execution. 252 | 253 | Following example shows the usage of `logging_enabled` = `false` and `get_disabled_logger()` in two unit tests. 254 | 255 | ```python 256 | 257 | from SomeModule import my_method 258 | import uuid 259 | def test_my_method(): 260 | config = { 261 | "log_level": "DEBUG", 262 | "logging_enabled": "false", 263 | "app_insights_key": str(uuid.uuid1()), 264 | } 265 | component_name = "TestComponent" 266 | app_logger = AppLogger( 267 | config=config 268 | ) 269 | assert app_logger is not None 270 | logger = app_logger.get_logger(component_name=component_name) 271 | assert logger is not None 272 | 273 | def test_my_method(): 274 | app_logger = get_disabled_logger() 275 | logger = app_logger.get_logger() 276 | assert logger is not None 277 | 278 | ``` 279 | 280 | ### Usage 281 | 282 | Here are steps required to use AppLogger 283 | 284 | 1. Follow Prerequisites mentioned in [section.](#Prerequisites) 285 | 2. Install pip packages 286 | 287 | ```bash 288 | pip install -r .\monitoring\requirements.txt 289 | ``` 290 | 291 | 3. Checkout [examples](./monitoring/examples/README.md) to see the usage of AppLogger 292 | 293 | 4. To execute unit test, use below command: 294 | 295 | ```bash 296 | python -m unittest discover .\monitoring\tests\ 297 | ``` 298 | 299 | ### Examples 300 | 301 | Examples created using AppLogger can be found [here](./monitoring/examples/README.md) 302 | 303 | 304 | [1]: https://docs.microsoft.com/azure/azure-monitor/app/opencensus-python 305 | [2]: https://opencensus.io/ 306 | [3]: https://docs.microsoft.com/azure/azure-monitor/app/app-insights-overview 307 | [4]: https://docs.microsoft.com/azure/azure-monitor/app/app-map?tabs=net#understanding-cloud-role-name-within-the-context-of-the-application-map 308 | [5]: https://github.com/census-instrumentation/opencensus-python/blob/master/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py 309 | [6]: https://docs.microsoft.com/azure/azure-monitor/app/correlation#role-names 310 | [7]: https://docs.microsoft.com/azure/azure-monitor/app/opencensus-python-dependency 311 | 312 | ### Contributors 313 | 314 | [Julien Chomarat](https://github.com/jchomarat) 315 | [Benjamin Guinebertière](https://github.com/benjguin) 316 | [Ankit Sinha](https://github.com/ankitbko) 317 | [Prabal Deb](https://github.com/prabdeb) 318 | [Megha Patil](https://github.com/meghapatilcode) 319 | [Srikantan Sankaran](https://github.com/ssrikantan) 320 | [Frédéric Le Coquil](https://github.com/flecoqui) 321 | [Anand Chugh](https://github.com/anandchugh) 322 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This document will give the overview about examples created using AppLogger class. 4 | 5 | ## Examples 6 | 7 | 1. **api_1.py**: This example contains the code showing the usage of disabled logger. 8 | 1. **api_2.py**: This is a flask api which exposes one rest end point and uses AppLogger 9 | 1. **api_3.py**: This is a flask api which exposes one rest end point and uses AppLogger. This rest end point calls: 10 | 1. Rest end point exposed in **api_2.py** 11 | 1. It also calls task1, which is an internal function. 12 | 1. It also calls task2, which is also an internal function. 13 | 1. **client**: This is client code which calls: 14 | 1. Rest end point exposed in **api_3.py** 15 | 1. Also calls `util_func` function in **util** 16 | It passes the tracer created in `client.py` to util_func as a parent_tracer. 17 | 1. **api_4.py**: This example contains the code showing the usage of event logger. 18 | 19 | ## Usage 20 | 21 | Following are the steps to execute the examples: 22 | 23 | 1. Follow Prerequisites mentioned in [section.](../../README.md#prerequisites-for-using-applogger) 24 | 25 | 1. Get Application Insights Instrumentation Key (app_insights_instrumentation_key) and update it in **logging_config.json** 26 | 27 | ```json 28 | { 29 | "logging_enabled":"true", 30 | "log_level":"INFO", 31 | "app_insights_key":"" 32 | } 33 | ``` 34 | 35 | 3. Install pip packages 36 | 37 | ```bash 38 | pip install -r .\monitoring\requirements.txt 39 | ``` 40 | 41 | 4. Execute examples in following sequence: 42 | 43 | 1. Run api_2.py flask app 44 | 45 | ```bash 46 | python .\monitoring\examples\api_2.py 47 | ``` 48 | 49 | 2. Run api_3.py flask app 50 | 51 | ```bash 52 | python .\monitoring\examples\api_3.py 53 | ``` 54 | 55 | 3. Run client.py 56 | 57 | ```bash 58 | python .\monitoring\examples\client.py 59 | ``` 60 | 4. Run api_4.py flask app 61 | 62 | ```bash 63 | python .\monitoring\examples\api_4.py 64 | ``` 65 | 66 | ## Results of executing examples: 67 | 68 | 1. Execution of `client.py` produces some logs on console 69 | ```sh 70 | ❯ python .\monitoring\examples\client.py 71 | 2021-04-13 14:58:26,399 name=client level=INFO traceId=3ac9716f42b11eb4c9d895b1ce528519 spanId=0000000000000000 Calling api 3 72 | response = b'{\n "data": "Success API3"\n}\n' 73 | 2021-04-13 14:58:31,300 name=util level=INFO traceId=3ac9716f42b11eb4c9d895b1ce528519 spanId=d0c25ebf22957d7e In util_func 74 | cess API3"\n}\n 75 | ``` 76 | 77 | > Note the traceId mentioned in above logs `3ac9716f42b11eb4c9d895b1ce528519` 78 | 79 | This `traceid` can be used to filter logs in application insights. 80 | 81 | 2. Use following Kusto query to filter logs in application insights using traceId got in previous step. 82 | 83 | ```py 84 | traces 85 | | where operation_Id == '' 86 | 87 | ``` 88 | 89 | 3. Use following Kusto query to filter dependency tracing using traceId got in the first step. 90 | 91 | ```py 92 | dependencies 93 | | where operation_Id == '' 94 | 95 | ``` 96 | 97 | 4. Use following Kusto queries to filter custom events logged. 98 | 99 | To find number of times API4 is triggered: 100 | 101 | ```kusto 102 | customEvents 103 | | where name == 'Start_API4' 104 | 105 | ``` 106 | To find execution time for API4: 107 | 108 | ```kusto 109 | customEvents 110 | | where name == 'API4_Execution_Time' 111 | | project name, customDimensions.execution_time 112 | 113 | ``` 114 | To find the data inside JSON strings returned: 115 | 116 | ```kusto 117 | customEvents 118 | | where name == 'API4_Return_Json' 119 | | extend parsed_json = parse_json(tostring(customDimensions.response)) 120 | | project name, parsed_json["data"] 121 | 122 | ``` 123 | 124 | 5. Verify application map in application insights. 125 | ![alt text](../img/application_map.png) 126 | 127 | ## Appendix: 128 | 129 | The following table presents various variations of event logging and their corresponding definitions and use cases. Event logging is a crucial tool for collecting and analyzing data in order to make informed decisions and improve business outcomes. By understanding the different types of event logging variations and when to use them, organizations can better capture and analyze important data points to drive their business forward. 130 | 131 | | Event logging variation | What it is | Example use cases | 132 | | ------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------| 133 | | Plain Event | Event logged without any custom_dimension | Success & failure counts, start & end events | 134 | | Event with a simple custom dimension | Event with custom_dimension with a simple type like number or string | Execution time, number of tokens in a completion, number of completions in case of LLM calls | 135 | | Event with a complex custom dimension | Event with custom_dimension with a complex type like a json object, dictionary or list etc | To log JSON objects like REST API responses, evaluation metrics in JSON format in case of LLM calls | 136 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/examples/__init__.py -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/api_1.py: -------------------------------------------------------------------------------- 1 | """REST API Module using AppLogger""" 2 | 3 | import json 4 | from flask import Flask, jsonify 5 | import sys 6 | import os 7 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 8 | 9 | from src.logger import AppLogger, get_disabled_logger 10 | component_name ="API_1" 11 | 12 | logger = get_disabled_logger().get_logger(component_name=component_name) 13 | 14 | app = Flask(component_name) 15 | 16 | @app.route('/', methods=['GET']) 17 | def home(): 18 | """End point for API1 19 | 20 | Returns: 21 | [Json]: [{'data': ''}] 22 | """ 23 | logger.info("In API1 home function") 24 | return jsonify({'data': 'Success API1'}) 25 | 26 | app.run(host="localhost", port=8000, debug=True) 27 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/api_2.py: -------------------------------------------------------------------------------- 1 | """REST API Module using AppLogger""" 2 | 3 | import json 4 | import logging 5 | from flask import Flask, jsonify 6 | import sys 7 | import os 8 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 9 | 10 | from src.logger import AppLogger 11 | component_name ="API_2" 12 | app = Flask(component_name) 13 | 14 | logging_config_file_path = os.path.join(os.getcwd(),'monitoring',"examples","logging_config.json") 15 | with open(logging_config_file_path) as logging_config_file: 16 | logging_config = json.load(logging_config_file) 17 | app_logger = AppLogger(config=logging_config) 18 | 19 | app_logger.enable_flask(flask_app=app,component_name= component_name) 20 | logger = app_logger.get_logger(component_name=component_name) 21 | tracer = app_logger.get_tracer(component_name=component_name) 22 | 23 | @app.route('/', methods=['GET']) 24 | def home(): 25 | """End point for API2 26 | 27 | Returns: 28 | [Json]: [{'data': ''}] 29 | """ 30 | logger.info("In API2 home function") 31 | return jsonify({'data': 'Success API2'}) 32 | 33 | app.run(host="localhost", port=8100, debug=True) 34 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/api_3.py: -------------------------------------------------------------------------------- 1 | """REST API Module using AppLogger""" 2 | import logging 3 | import json 4 | from flask import Flask, jsonify 5 | import requests 6 | import sys 7 | import os 8 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 9 | 10 | from src.logger import AppLogger 11 | component_name ="API_3" 12 | app = Flask(component_name) 13 | 14 | logging_config_file_path = os.path.join(os.getcwd(),'monitoring',"examples","logging_config.json") 15 | with open(logging_config_file_path) as logging_config_file: 16 | logging_config = json.load(logging_config_file) 17 | app_logger = AppLogger(config=logging_config) 18 | 19 | app_logger.enable_flask(flask_app=app,component_name= component_name) 20 | logger = app_logger.get_logger(component_name=component_name) 21 | tracer = app_logger.get_tracer(component_name=component_name) 22 | 23 | @app.route('/', methods=['GET']) 24 | def home(): 25 | """End point for API3 26 | 27 | Returns: 28 | [Json]: [{'data': ''}] 29 | """ 30 | logger.info("In API3 home function") 31 | 32 | with tracer.span("API3_Task1"): 33 | ret_val_1 = task1() 34 | 35 | with tracer.span("API3_Task2"): 36 | ret_val_2 = task2() 37 | 38 | logger.info("Calling API 2") 39 | response = requests.get(url='http://localhost:8100/') 40 | print(f"response = {response.content}") 41 | 42 | return jsonify({'data': 'Success API3'}) 43 | 44 | def task1(): 45 | """Task1 function of API3 46 | 47 | Returns: 48 | [str]: [Return string] 49 | """ 50 | logger.info("In API3 task1 function") 51 | return "task1 success!" 52 | 53 | 54 | def task2(): 55 | """Task1 function of API2 56 | 57 | Returns: 58 | [str]: [Return string] 59 | """ 60 | logger.info("In API3 task2 function") 61 | return "task2 success!" 62 | 63 | if __name__ == "__main__": 64 | app.run(host="localhost", port=8300,debug=True) -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/api_4.py: -------------------------------------------------------------------------------- 1 | """REST API Module using AppLogger""" 2 | 3 | import json 4 | import logging 5 | from flask import Flask, jsonify 6 | import sys 7 | import os 8 | import time 9 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 10 | 11 | from src.logger import AppLogger 12 | component_name ="API_4" 13 | app = Flask(component_name) 14 | 15 | logging_config_file_path = os.path.join(os.getcwd(),'monitoring',"examples","logging_config.json") 16 | with open(logging_config_file_path) as logging_config_file: 17 | logging_config = json.load(logging_config_file) 18 | app_logger = AppLogger(config=logging_config) 19 | 20 | app_logger.enable_flask(flask_app=app,component_name= component_name) 21 | event_logger = app_logger.get_event_logger(component_name=component_name) 22 | 23 | @app.route('/', methods=['GET']) 24 | def home(): 25 | """End point for API4 26 | 27 | Returns: 28 | [Json]: [{'data': ''}] 29 | """ 30 | #Plain Event 31 | event_logger.info("Start_API4") 32 | 33 | start_time = time.time() 34 | jsonified_data= jsonify({'data': 'Success API4'}) 35 | 36 | execution_time=time.time() - start_time 37 | extra_params = {"custom_dimensions": {"execution_time":execution_time}} 38 | 39 | #Event with an additional custom dimension execution_time 40 | event_logger.info("API4_Execution_Time", extra=extra_params) 41 | 42 | extra_params = {"custom_dimensions": {"response":jsonified_data}} 43 | 44 | #Event with an additional custom dimension with json value 45 | event_logger.info("API4_Return_Json", extra=extra_params) 46 | 47 | return jsonified_data 48 | 49 | app.run(host="localhost", port=8100, debug=True) 50 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/client.py: -------------------------------------------------------------------------------- 1 | """"REST API Client Module using AppLogger""" 2 | import requests 3 | import logging 4 | import sys 5 | import os 6 | import pathlib 7 | import json 8 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 9 | 10 | from src.logger import AppLogger 11 | from util import util_func 12 | 13 | component_name = "client" 14 | 15 | logging_config_file_path = os.path.join(os.getcwd(),'monitoring',"examples","logging_config.json") 16 | with open(logging_config_file_path) as logging_config_file: 17 | logging_config = json.load(logging_config_file) 18 | 19 | app_logger = AppLogger(config=logging_config) 20 | logger = app_logger.get_logger(component_name=component_name) 21 | tracer = app_logger.get_tracer(component_name=component_name) 22 | 23 | def call_api_3(): 24 | """Function calling endpoint of API3 25 | """ 26 | logger.info("Calling api 3") 27 | response = requests.get(url='http://localhost:8300/') 28 | print(f"response = {response.content}") 29 | 30 | with tracer.span("util_func"): 31 | util_func(app_logger=app_logger,parent_tracer=tracer) 32 | 33 | if __name__ == '__main__': 34 | call_api_3() -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/logging_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "logging_enabled":"true", 3 | "log_level":"INFO", 4 | "app_insights_key":"" 5 | } -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/examples/util.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import os 4 | sys.path.append(os.path.join(os.getcwd(),'monitoring')) 5 | 6 | from src.logger import AppLogger, get_disabled_logger 7 | from opencensus.trace.tracer import Tracer 8 | 9 | component_name="util" 10 | 11 | def util_func(app_logger=get_disabled_logger(), parent_tracer=None): 12 | """Util function 13 | 14 | Args: 15 | app_logger (AppLogger, optional): AppLogger Object. Defaults to get_disabled_logger(). 16 | parent_tracer (Tracer, optional): Parent tracer. Defaults to None. 17 | """ 18 | logger = app_logger.get_logger(component_name=component_name) 19 | tracer = app_logger.get_tracer(parent_tracer=parent_tracer ) 20 | logger.info("In util_func") 21 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/img/application_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/img/application_map.png -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/img/dependency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/img/dependency.png -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/img/operation_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/img/operation_id.png -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/requirements.txt: -------------------------------------------------------------------------------- 1 | opencensus-ext-azure 2 | opencensus-ext-logging 3 | opencensus-ext-flask 4 | opencensus-ext-requests 5 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/src/__init__.py -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/src/logger.py: -------------------------------------------------------------------------------- 1 | """This module is used to log traces into Azure Application Insights.""" 2 | import functools 3 | import logging 4 | import uuid 5 | from os import getenv 6 | 7 | from opencensus.ext.azure.common import utils 8 | from opencensus.ext.azure.log_exporter import AzureLogHandler, AzureEventHandler 9 | from opencensus.ext.azure.trace_exporter import AzureExporter 10 | from opencensus.trace import config_integration 11 | from opencensus.trace.samplers import AlwaysOffSampler, AlwaysOnSampler 12 | from opencensus.trace.tracer import Tracer 13 | from opencensus.ext.flask.flask_middleware import FlaskMiddleware 14 | from opencensus.trace.samplers import ProbabilitySampler 15 | 16 | 17 | class CustomDimensionsFilter(logging.Filter): 18 | """Add custom-dimensions in each log by using filters.""" 19 | 20 | def __init__(self, custom_dimensions=None): 21 | """Initialize CustomDimensionsFilter.""" 22 | self.custom_dimensions = custom_dimensions or {} 23 | 24 | def filter(self, record): 25 | """Add the default custom_dimensions into the current log record.""" 26 | dim = {**self.custom_dimensions, **getattr(record, "custom_dimensions", {})} 27 | record.custom_dimensions = dim 28 | return True 29 | 30 | 31 | class AppLogger: 32 | """Logger wrapper that attach the handler to Application Insights.""" 33 | 34 | HANDLER_NAME = "Azure Application Insights Handler" 35 | 36 | EVENT_HANDLER_NAME = "Azure Application Insights Event Handler" 37 | 38 | def __init__(self, config=None): 39 | """Create an instance of the Logger class. 40 | 41 | Args: 42 | config:([dict], optional): 43 | Contains the setting for logger {"log_level": "DEBUG","logging_enabled":"true"", 44 | "app_insights_key":""} 45 | """ 46 | config_integration.trace_integrations(["logging"]) 47 | config_integration.trace_integrations(['requests']) 48 | self.config = {"log_level": logging.INFO, "logging_enabled": "true"} 49 | self.APPINSIGHTS_INSTRUMENTATION_KEY = "APPINSIGHTS_INSTRUMENTATION_KEY" 50 | self.update_config(config) 51 | 52 | def _initialize_azure_log_handler(self, component_name, custom_dimensions): 53 | """Initialize azure log handler.""" 54 | # Adding logging to trace_integrations 55 | # This will help in adding trace and span ids to logs 56 | # https://github.com/census-instrumentation/opencensus-python/tree/master/contrib/opencensus-ext-logging 57 | 58 | logging.basicConfig( 59 | format="%(asctime)s name=%(name)s level=%(levelname)s " 60 | "traceId=%(traceId)s spanId=%(spanId)s %(message)s" 61 | ) 62 | app_insights_cs = "InstrumentationKey=" + self._get_app_insights_key() 63 | log_handler = AzureLogHandler( 64 | connection_string=app_insights_cs, export_interval=5.0 65 | ) 66 | log_handler.add_telemetry_processor(self._get_callback(component_name)) 67 | log_handler.name = self.HANDLER_NAME 68 | log_handler.addFilter(CustomDimensionsFilter(custom_dimensions)) 69 | return log_handler 70 | 71 | def _initialize_azure_event_handler(self, component_name, custom_dimensions): 72 | """Initialize azure event handler.""" 73 | app_insights_cs = "InstrumentationKey=" + self._get_app_insights_key() 74 | event_handler = AzureEventHandler( 75 | connection_string=app_insights_cs, export_interval=5.0 76 | ) 77 | event_handler.add_telemetry_processor(self._get_callback(component_name)) 78 | event_handler.name = self.EVENT_HANDLER_NAME 79 | event_handler.addFilter(CustomDimensionsFilter(custom_dimensions)) 80 | return event_handler 81 | 82 | def _get_trace_exporter(self, component_name="AppLogger"): 83 | """[Get log exporter] 84 | 85 | Returns: 86 | [AzureExporter]: [Azure Trace Exporter] 87 | """ 88 | app_insights_cs = "InstrumentationKey=" + self._get_app_insights_key() 89 | log_exporter = AzureExporter( 90 | connection_string=app_insights_cs, export_interval=0.0 91 | ) 92 | log_exporter.add_telemetry_processor(self._get_callback(component_name)) 93 | return log_exporter 94 | 95 | def _initialize_logger(self, log_handler, component_name): 96 | """Initialize Logger.""" 97 | logger = logging.getLogger(component_name) 98 | logger.setLevel(self.log_level) 99 | if self.config.get("logging_enabled") == "true": 100 | if not any(x for x in logger.handlers if x.name == self.HANDLER_NAME or x.name == self.EVENT_HANDLER_NAME): 101 | logger.addHandler(log_handler) 102 | return logger 103 | 104 | def get_logger(self, component_name="AppLogger", custom_dimensions={}): 105 | """Get Logger Object. 106 | 107 | Args: 108 | component_name (str, optional): Name of logger. Defaults to "AppLogger". 109 | custom_dimensions (dict, optional): {"key":"value"} to capture with every log. 110 | Defaults to {}. 111 | 112 | Returns: 113 | Logger: A logger. 114 | """ 115 | self.update_config(self.config) 116 | handler = self._initialize_azure_log_handler(component_name, custom_dimensions) 117 | return self._initialize_logger(handler, component_name) 118 | 119 | def get_event_logger(self, component_name="AppLogger", custom_dimensions={}): 120 | """Get Event ogger Object. 121 | 122 | Args: 123 | component_name (str, optional): Name of logger. Defaults to "AppLogger". 124 | custom_dimensions (dict, optional): {"key":"value"} to capture with every log. 125 | Defaults to {}. 126 | 127 | Returns: 128 | Logger: A logger. 129 | """ 130 | self.update_config(self.config) 131 | handler = self._initialize_azure_event_handler(component_name, custom_dimensions) 132 | return self._initialize_logger(handler, component_name) 133 | 134 | def get_tracer(self, component_name="AppLogger", parent_tracer=None): 135 | """Get Tracer Object. 136 | 137 | Args: 138 | component_name (str, optional): Name of logger. Defaults to "AppLogger". 139 | parent_tracer([opencensus.trace.tracer], optional): 140 | Contains parent tracer required for setting coorelation. 141 | 142 | Returns: 143 | opencensus.trace.tracer: A Tracer. 144 | """ 145 | self.update_config(self.config) 146 | sampler = AlwaysOnSampler() 147 | exporter = self._get_trace_exporter(component_name) 148 | if self.config.get("logging_enabled") != "true": 149 | sampler = AlwaysOffSampler() 150 | if parent_tracer is None: 151 | tracer = Tracer(exporter=exporter, sampler=sampler) 152 | else: 153 | tracer = Tracer( 154 | span_context=parent_tracer.span_context, 155 | exporter=exporter, 156 | sampler=sampler, 157 | ) 158 | return tracer 159 | 160 | def enable_flask(self,flask_app,component_name="AppLogger"): 161 | """Enable flask for tracing 162 | For more info : https://github.com/census-instrumentation/opencensus-python/blob/master/contrib/opencensus-ext-flask/opencensus/ext/flask/flask_middleware.py 163 | 164 | Args: 165 | flask_app ([type]): [description] 166 | component_name (str, optional): [description]. Defaults to "AppLogger". 167 | """ 168 | FlaskMiddleware( 169 | flask_app, exporter=self._get_trace_exporter(component_name=component_name) 170 | ) 171 | 172 | def _get_app_insights_key(self): 173 | """Get Application Insights Key.""" 174 | try: 175 | if self.app_insights_key is None: 176 | self.app_insights_key = getenv( 177 | self.APPINSIGHTS_INSTRUMENTATION_KEY, None 178 | ) 179 | if self.app_insights_key is not None: 180 | utils.validate_instrumentation_key(self.app_insights_key) 181 | return self.app_insights_key 182 | else: 183 | raise Exception("ApplicationInsights Key is not set") 184 | except Exception as exp: 185 | raise Exception(f"Exception is getting app insights key-> {exp}") 186 | 187 | def _get_callback(self, component_name): 188 | """Adding cloud role name. This is required to give the name of component in application map. 189 | https://docs.microsoft.com/azure/azure-monitor/app/app-map?tabs=net#understanding-cloud-role-name-within-the-context-of-the-application-map 190 | 191 | Args: 192 | component_name ([str]): [The name of the component or applicaiton] 193 | """ 194 | def _callback_add_role_name(envelope): 195 | """Add role name for logger.""" 196 | envelope.tags["ai.cloud.role"] = component_name 197 | envelope.tags["ai.cloud.roleInstance"] = component_name 198 | 199 | return _callback_add_role_name 200 | 201 | def update_config(self, config=None): 202 | """Update logger configuration.""" 203 | if config is not None: 204 | self.config.update(config) 205 | self.app_insights_key = self.config.get("app_insights_key") 206 | self.log_level = self.config.get("log_level") 207 | 208 | 209 | def get_disabled_logger(): 210 | """Get a disabled AppLogger. 211 | 212 | Returns: 213 | AppLogger: A disabled AppLogger 214 | """ 215 | return AppLogger( 216 | config={"logging_enabled": "false", "app_insights_key": str(uuid.uuid1())} 217 | ) 218 | -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-monitor-opencensus-python/32a22d93b315ec04e68a1bb5d3464f029fbf9649/azure_monitor/python_logger_opencensus_azure/monitoring/tests/__init__.py -------------------------------------------------------------------------------- /azure_monitor/python_logger_opencensus_azure/monitoring/tests/test_logger.py: -------------------------------------------------------------------------------- 1 | """Test src/app_logger.py.""" 2 | 3 | import logging 4 | import uuid 5 | 6 | import unittest 7 | from monitoring.src.logger import AppLogger, get_disabled_logger 8 | 9 | test_instrumentation_key = str(uuid.uuid1()) 10 | test_invalid_instrumentation_key = "invalid_instrumentation_key" 11 | 12 | 13 | class TestAppLogger(unittest.TestCase): 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | cls.valid_config = { 18 | "log_level": "DEBUG", 19 | "logging_enabled": "true", 20 | "app_insights_key": test_instrumentation_key, 21 | } 22 | cls.invalid_config = { 23 | "log_level": "DEBUG", 24 | "logging_enabled": "false", 25 | "app_insights_key": test_invalid_instrumentation_key, 26 | } 27 | 28 | 29 | def test_logger_creation_valid_instrumentation_key(self): 30 | """Test with valid formatted instrumentation key.""" 31 | global test_instrumentation_key 32 | try: 33 | app_logger = AppLogger( 34 | config=self.valid_config, 35 | ) 36 | assert app_logger is not None 37 | except Exception: 38 | assert False 39 | 40 | 41 | def test_logger_creation_invalid_instrumentation_key(self): 42 | """Test with invalid instrumentation key.""" 43 | global test_invalid_instrumentation_key 44 | with self.assertRaises(Exception): 45 | logging.disable(logging.CRITICAL) 46 | app_logger = AppLogger( 47 | config=self.invalid_config, 48 | ) 49 | app_logger.get_logger() 50 | assert app_logger is not None 51 | 52 | 53 | def test_logger_creation_no_instrumentation_key(self): 54 | """Test with no instrumentation key.""" 55 | with self.assertRaises(Exception): 56 | logging.disable(logging.CRITICAL) 57 | config = {"log_level": logging.DEBUG, "logging_enabled": "false"} 58 | app_logger = AppLogger(config=config) 59 | app_logger.get_logger() 60 | assert app_logger is not None 61 | 62 | 63 | def test_logging(self): 64 | """Test to use logging functions.""" 65 | global test_instrumentation_key 66 | try: 67 | component_name = "TestComponent" 68 | app_logger = AppLogger(config=self.valid_config) 69 | assert app_logger is not None 70 | test_logger = app_logger.get_logger( 71 | component_name=component_name, 72 | ) 73 | 74 | assert test_logger is not None 75 | test_logger.info("Test Logging") 76 | except Exception: 77 | assert False 78 | 79 | def test_event_logging(self): 80 | """Test to use event logging functions.""" 81 | global test_instrumentation_key 82 | try: 83 | component_name = "TestComponent" 84 | app_logger = AppLogger(config=self.valid_config) 85 | assert app_logger is not None 86 | test_event_logger = app_logger.get_event_logger( 87 | component_name=component_name, 88 | ) 89 | 90 | assert test_event_logger is not None 91 | test_event_logger.info("Test Event Logging") 92 | except Exception: 93 | assert False 94 | 95 | def test_tracing(self): 96 | """Test for Tracer.""" 97 | global test_instrumentation_key 98 | try: 99 | component_name = "TestComponent" 100 | app_logger = AppLogger(config=self.valid_config) 101 | assert app_logger is not None 102 | 103 | tracer = app_logger.get_tracer( 104 | component_name=component_name, 105 | ) 106 | test_logger = app_logger.get_logger( 107 | component_name=component_name, 108 | ) 109 | 110 | assert test_logger is not None 111 | assert tracer is not None 112 | 113 | with tracer.span(name="testspan"): 114 | test_logger.info("in test span") 115 | except Exception: 116 | assert False 117 | 118 | 119 | def test_exception(self): 120 | """Test for calling logger.exception method.""" 121 | global test_instrumentation_key 122 | try: 123 | component_name = "TestComponent" 124 | app_logger = AppLogger( 125 | config=self.valid_config, 126 | ) 127 | assert app_logger is not None 128 | 129 | test_logger = app_logger.get_logger( 130 | component_name=component_name, 131 | ) 132 | assert test_logger is not None 133 | try: 134 | logging.disable(logging.CRITICAL) 135 | raise Exception("Testing exception logging") 136 | except Exception as exp: 137 | test_logger.exception(exp) 138 | except Exception: 139 | assert False 140 | 141 | 142 | def test_logging_level(self): 143 | """Test for changing logger level in config.""" 144 | try: 145 | global test_instrumentation_key 146 | component_name = "TestComponent" 147 | valid_config = self.valid_config.copy() 148 | valid_config["log_level"] = logging.ERROR 149 | app_logger = AppLogger( 150 | config=valid_config, 151 | ) 152 | assert app_logger.config["log_level"] == logging.ERROR 153 | test_logger = app_logger.get_logger( 154 | component_name=component_name, 155 | ) 156 | 157 | test_logger.error("Testing logging level") 158 | except Exception: 159 | assert False 160 | 161 | 162 | def test_logging_extra_params(self): 163 | """Test logging extra params.""" 164 | try: 165 | global test_instrumentation_key 166 | component_name = "TestComponent" 167 | app_logger = AppLogger( 168 | config=self.valid_config, 169 | ) 170 | test_logger = app_logger.get_logger( 171 | component_name=component_name, 172 | ) 173 | extra_params = {"custom_dimensions": {"key1": "value1"}} 174 | test_logger.info("Logging extra params", extra=extra_params) 175 | except Exception: 176 | assert False 177 | 178 | def test_event_logging_extra_params(self): 179 | """Test event logging extra params.""" 180 | try: 181 | global test_instrumentation_key 182 | component_name = "TestComponent" 183 | app_logger = AppLogger( 184 | config=self.valid_config, 185 | ) 186 | test_event_logger = app_logger.get_event_logger( 187 | component_name=component_name, 188 | ) 189 | extra_params = {"custom_dimensions": {"key1": "value1"}} 190 | test_event_logger.info("Event Logging with extra params", extra=extra_params) 191 | except Exception: 192 | assert False 193 | 194 | def test_disabled_logger(self): 195 | """Test disabled logger.""" 196 | try: 197 | 198 | def do_work(app_logger=get_disabled_logger()): 199 | component_name = "TestComponent" 200 | test_logger = app_logger.get_logger( 201 | component_name=component_name, 202 | ) 203 | extra_params = {"custom_dimensions": {"key1": "value1"}} 204 | test_logger.info("Logging extra params", extra=extra_params) 205 | 206 | do_work() 207 | except Exception: 208 | assert False 209 | 210 | 211 | if __name__ == '__main__': 212 | unittest.main() 213 | 214 | -------------------------------------------------------------------------------- /azure_monitor/simple_sample/customDimensions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from opencensus.ext.azure.log_exporter import AzureLogHandler 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 9 | 10 | logger.addHandler(AzureLogHandler( 11 | connection_string=appKey) 12 | ) 13 | 14 | properties = {'custom_dimensions': {'key_1': 'value_1', 'key_2': 'value_2'}} 15 | 16 | # Use properties in logging statements 17 | logger.warning('action', extra=properties) -------------------------------------------------------------------------------- /azure_monitor/simple_sample/database.py: -------------------------------------------------------------------------------- 1 | import pyodbc 2 | import os 3 | 4 | server = os.getenv('DB_SERVER') 5 | database = os.getenv('DB_DATABASE') 6 | username = os.getenv('DB_USERNAME') 7 | password = os.getenv('DB_PASSWORD') 8 | driver= '{ODBC Driver 17 for SQL Server}' 9 | 10 | with pyodbc.connect('DRIVER='+driver+';SERVER=tcp:'+server+';PORT=1433;DATABASE='+database+';UID='+username+';PWD='+ password) as conn: 11 | with conn.cursor() as cursor: 12 | cursor.execute("SELECT TOP 3 name, collation_name FROM sys.databases") 13 | row = cursor.fetchone() 14 | while row: 15 | print (str(row[0]) + " " + str(row[1])) 16 | row = cursor.fetchone() -------------------------------------------------------------------------------- /azure_monitor/simple_sample/event.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019, OpenCensus Authors 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | from opencensus.ext.azure.log_exporter import AzureEventHandler 18 | 19 | logger = logging.getLogger(__name__) 20 | # TODO: you need to specify the instrumentation key in a connection string 21 | # and place it in the APPLICATIONINSIGHTS_CONNECTION_STRING 22 | # environment variable. 23 | logger.addHandler(AzureEventHandler()) 24 | logger.setLevel(logging.INFO) 25 | logger.info('Hello, World!') 26 | -------------------------------------------------------------------------------- /azure_monitor/simple_sample/metric.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from opencensus.ext.azure import metrics_exporter 4 | from opencensus.stats import aggregation as aggregation_module 5 | from opencensus.stats import measure as measure_module 6 | from opencensus.stats import stats as stats_module 7 | from opencensus.stats import view as view_module 8 | from opencensus.tags import tag_map as tag_map_module 9 | 10 | stats = stats_module.stats 11 | view_manager = stats.view_manager 12 | stats_recorder = stats.stats_recorder 13 | 14 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 15 | 16 | CARROTS_MEASURE = measure_module.MeasureInt("carrots", 17 | "number of carrots", 18 | "carrots") 19 | CARROTS_VIEW = view_module.View("carrots_view", 20 | "number of carrots", 21 | [], 22 | CARROTS_MEASURE, 23 | aggregation_module.CountAggregation()) 24 | 25 | # Enable metrics 26 | # Set the interval in seconds in which you want to send metrics 27 | exporter = metrics_exporter.new_metrics_exporter(connection_string=appKey) 28 | view_manager.register_exporter(exporter) 29 | 30 | view_manager.register_view(CARROTS_VIEW) 31 | mmap = stats_recorder.new_measurement_map() 32 | tmap = tag_map_module.TagMap() 33 | 34 | def prompt(): 35 | mmap.measure_int_put(CARROTS_MEASURE, 1000) 36 | mmap.record(tmap) 37 | # Default export interval is every 15.0s 38 | 39 | def main(): 40 | while True: 41 | prompt() 42 | 43 | print("Done recording metrics") 44 | 45 | if __name__ == "__main__": 46 | main() -------------------------------------------------------------------------------- /azure_monitor/simple_sample/metric2.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from datetime import datetime 4 | from opencensus.ext.azure import metrics_exporter 5 | from opencensus.stats import aggregation as aggregation_module 6 | from opencensus.stats import measure as measure_module 7 | from opencensus.stats import stats as stats_module 8 | from opencensus.stats import view as view_module 9 | from opencensus.tags import tag_map as tag_map_module 10 | 11 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 12 | 13 | stats = stats_module.stats 14 | view_manager = stats.view_manager 15 | stats_recorder = stats.stats_recorder 16 | 17 | prompt_measure = measure_module.MeasureInt("prompts", 18 | "number of prompts", 19 | "prompts") 20 | prompt_view = view_module.View("prompt_view", 21 | "number of prompts", 22 | [], 23 | prompt_measure, 24 | aggregation_module.CountAggregation()) 25 | exporter = metrics_exporter.new_metrics_exporter(connection_string=appKey) 26 | 27 | view_manager.register_view(prompt_view) 28 | mmap = stats_recorder.new_measurement_map() 29 | tmap = tag_map_module.TagMap() 30 | 31 | view_manager.register_exporter(exporter) 32 | 33 | def prompt(): 34 | mmap.measure_int_put(prompt_measure, 1) 35 | mmap.record(tmap) 36 | metrics = list(mmap.measure_to_view_map.get_metrics(datetime.utcnow())) 37 | print(metrics[0].time_series[0].points[0]) 38 | 39 | def main(): 40 | while True: 41 | prompt() 42 | 43 | if __name__ == "__main__": 44 | main() -------------------------------------------------------------------------------- /azure_monitor/simple_sample/module1.py: -------------------------------------------------------------------------------- 1 | # module1.py 2 | import logging 3 | import os 4 | 5 | from opencensus.trace import config_integration 6 | from opencensus.trace.samplers import AlwaysOnSampler 7 | from opencensus.trace.tracer import Tracer 8 | from opencensus.ext.azure.log_exporter import AzureLogHandler 9 | from module2 import function_1 10 | 11 | config_integration.trace_integrations(['logging']) 12 | logging.basicConfig(format='%(asctime)s spanId=%(spanId)s %(message)s') 13 | tracer = Tracer(sampler=AlwaysOnSampler()) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 18 | 19 | logger.addHandler(AzureLogHandler( 20 | connection_string=appKey) 21 | ) 22 | 23 | logger.warning('Before the span') 24 | 25 | with tracer.span(name='hello'): 26 | logger.warning('In the span') 27 | function_1(tracer) 28 | 29 | logger.warning('After the span') -------------------------------------------------------------------------------- /azure_monitor/simple_sample/module2.py: -------------------------------------------------------------------------------- 1 | # module2.py 2 | 3 | import logging 4 | import os 5 | 6 | from opencensus.trace import config_integration 7 | from opencensus.trace.samplers import AlwaysOnSampler 8 | from opencensus.trace.tracer import Tracer 9 | from opencensus.ext.azure.log_exporter import AzureLogHandler 10 | 11 | config_integration.trace_integrations(['logging']) 12 | logging.basicConfig(format='%(asctime)s spanId=%(spanId)s %(message)s') 13 | tracer = Tracer(sampler=AlwaysOnSampler()) 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 18 | 19 | logger.addHandler(AzureLogHandler( 20 | connection_string=appKey) 21 | ) 22 | 23 | def function_1(parent_tracer=None): 24 | if parent_tracer is not None: 25 | tracer = Tracer( 26 | span_context=parent_tracer.span_context, 27 | sampler=AlwaysOnSampler(), 28 | ) 29 | else: 30 | tracer = Tracer(sampler=AlwaysOnSampler()) 31 | 32 | with tracer.span("function_1"): 33 | logger.info("In function_1") -------------------------------------------------------------------------------- /azure_monitor/simple_sample/prompt.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from opencensus.ext.azure.log_exporter import AzureLogHandler 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 8 | 9 | logger.addHandler(AzureLogHandler( 10 | connection_string=appKey) 11 | ) 12 | 13 | # You can also instantiate the exporter directly if you have the environment variable 14 | # `APPLICATIONINSIGHTS_CONNECTION_STRING` configured 15 | # logger.addHandler(AzureLogHandler()) 16 | 17 | def valuePrompt(): 18 | line = input("Enter a value: ") 19 | logger.warning(line) 20 | 21 | def main(): 22 | while True: 23 | valuePrompt() 24 | 25 | if __name__ == "__main__": 26 | main() -------------------------------------------------------------------------------- /azure_monitor/simple_sample/readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Sample: Simple Python Applications with OpenCensus' 3 | description: Simple Python Applications with OpenCensus. 4 | author: givenscj 5 | ms.author: givenscj 6 | ms.devlang: python 7 | ms.topic: sample 8 | ms.date: 07/27/2022 9 | ms.custom: [] 10 | --- 11 | 12 | # Sample: Simple Python Applications with OpenCensus 13 | 14 | The following steps walkthrough setting up simple Python examples and executing them in a local environment. 15 | 16 | ## 1 - Environment Setup 17 | 18 | Follow the steps in the [setup documentation](/azure_monitor/README.md). 19 | 20 | ## 2 - Create a simple Python App 21 | 22 | - Create a new python file called `./simple_sample/app.py`. 23 | - Copy the following into it, be sure to replace your Application Insights connection string that you copied above: 24 | 25 | ```python 26 | import logging 27 | 28 | from opencensus.ext.azure.log_exporter import AzureLogHandler 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | logger.addHandler(AzureLogHandler(connection_string='InstrumentationKey=')) 33 | 34 | logger.warning('Hello, World!') 35 | ``` 36 | 37 | - Press **F5** to run the file, select **Python file** in the debug configuration window. 38 | - If you get an error about `psutil._psutil_windows` do the following: 39 | - Open the `.venv\Lib\site-packages` folder and delete the `psutil` and `psutil-5.9.1.dist-info` folders. 40 | - Then run the following: 41 | 42 | ```Python 43 | python -m pip install --upgrade pip 44 | python -m pip install psutil 45 | ``` 46 | 47 | - Switch to the Azure Portal, navigate to your Application Insights resource. 48 | - Under **Monitoring**, select **Logs**. 49 | - Run the following kusto query: 50 | 51 | ```kusto 52 | traces 53 | | sort by timestamp 54 | | where cloud_RoleName == "app.py" 55 | ``` 56 | 57 | - You should see the following: 58 | 59 | ![The query is displayed with one result from the above program.](../media/python_simple_app_trace.png "Review the results of the query.") 60 | 61 | - Open the `./azure_monitor/simple_sample/metric.py` file. 62 | - Press **F5** to run the file, select **Python file** in the debug configuration window. 63 | - Switch to the Azure Portal. 64 | - Browse to your lab resource group. 65 | - Browse to the `python-appinsights-SUFFIX` application insights resource and select it. 66 | - Under **Monitoring**, select **Metrics**. 67 | - For the **Metric namespace**, select **carrots_view**. 68 | - For the **Metric**, select **carrots_view**. 69 | - You should see some data displayed: 70 | 71 | ![The custom metric for the carrots view is displayed.](../media/python_custommetrics-carrots.png "Review the results of the metric data.") 72 | 73 | - Stop the application. 74 | 75 | - You can user kusto queries to get metric data: 76 | - Under **Montioring**, select **Logs**. 77 | - Run the following query: 78 | 79 | ```kusto 80 | customMetrics 81 | | where cloud_RoleName == "metric.py" 82 | ``` 83 | 84 | - You should see the metric value is recorded every 15 seconds: 85 | 86 | ![The custom metric for the carrots view is displayed.](../media/python_custommetrics-carrots-logs.png "Review the results of the metric data.") 87 | 88 | ## 3 - Sending trace data 89 | 90 | - Open the `./azure_monitor/simple_sample/trace.py` file, notice that the connection string is being pulled from an environment variable rather than being hard coded. 91 | - Press **F5** to run the file. 92 | - Switch to the Azure Portal. 93 | - Browse to your lab resource group. 94 | - Browse to the `python-appinsights-SUFFIX` application insights resource and select it. 95 | - Under **Monitoring**, select **Logs**. 96 | - Run the following Kusto query to see the `traces` sent to application insights: 97 | 98 | ```kql 99 | traces 100 | | sort by timestamp 101 | | where cloud_RoleName == "trace.py" 102 | ``` 103 | 104 | - You should see your trace in the **message** column: 105 | 106 | ![The query is displayed with one result from the above program.](../media/python_simple_trace_trace.png "Review the results of the query.") 107 | 108 | - For your trace item notice that some of the columns are empty, but others (such as `cloud_RoleName`, `cloud_RoleInstance` and `client_*`) are populated based on the client information from the SDK. 109 | 110 | ## 4 - Capture exceptions and custom dimensions 111 | 112 | - Switch back to your Visual Studio Code window. 113 | - Select the `./simple_sample/properties.py` file. 114 | - Press **F5** to run the file, select **Python file** in the debug configuration window. 115 | - Switch to the Azure Portal. 116 | - Browse to your lab resource group. 117 | - Browse to the `python-appinsights-SUFFIX` application insights resource and select it. 118 | - Under **Monitoring**, select **Logs**. 119 | - Run the following Kusto query to see the `exception` sent to application insights: 120 | 121 | ```kql 122 | exceptions 123 | | sort by timestamp 124 | | where cloud_RoleName == "properties.py" 125 | ``` 126 | 127 | - Expand your exception item, notice the `details` column has the exception details you would expect, but also expand the `customDimensions` column to review the custom data that was sent to the application insights table. 128 | 129 | ![The query is displayed with one result from the above program.](../media/python_simple_exception_custom.png "Review the results of the query.") -------------------------------------------------------------------------------- /azure_monitor/simple_sample/spanComplex.py: -------------------------------------------------------------------------------- 1 | # module1.py 2 | import logging 3 | 4 | from opencensus.trace import config_integration 5 | from opencensus.trace.samplers import AlwaysOnSampler 6 | from opencensus.trace.tracer import Tracer 7 | from module2 import function_1 8 | 9 | config_integration.trace_integrations(['logging']) 10 | logging.basicConfig(format='%(asctime)s traceId=%(traceId)s spanId=%(spanId)s %(message)s') 11 | tracer = Tracer(sampler=AlwaysOnSampler()) 12 | 13 | logger = logging.getLogger(__name__) 14 | logger.warning('Before the span') 15 | with tracer.span(name='hello'): 16 | logger.warning('In the span') 17 | function_1(tracer) 18 | logger.warning('After the span') 19 | 20 | # module2.py 21 | 22 | import logging 23 | 24 | from opencensus.trace import config_integration 25 | from opencensus.trace.samplers import AlwaysOnSampler 26 | from opencensus.trace.tracer import Tracer 27 | 28 | config_integration.trace_integrations(['logging']) 29 | logging.basicConfig(format='%(asctime)s traceId=%(traceId)s spanId=%(spanId)s %(message)s') 30 | tracer = Tracer(sampler=AlwaysOnSampler()) 31 | 32 | def function_1(parent_tracer=None): 33 | if parent_tracer is not None: 34 | tracer = Tracer( 35 | span_context=parent_tracer.span_context, 36 | sampler=AlwaysOnSampler(), 37 | ) 38 | else: 39 | tracer = Tracer(sampler=AlwaysOnSampler()) 40 | 41 | with tracer.span("function_1"): 42 | logger.info("In function_1") -------------------------------------------------------------------------------- /azure_monitor/simple_sample/spanSimple.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from opencensus.trace import config_integration 5 | from opencensus.trace.samplers import AlwaysOnSampler 6 | from opencensus.trace.tracer import Tracer 7 | from opencensus.ext.azure.log_exporter import AzureLogHandler 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 12 | 13 | logger.addHandler(AzureLogHandler( 14 | connection_string=appKey) 15 | ) 16 | 17 | config_integration.trace_integrations(['logging']) 18 | #logging.basicConfig(format='%(asctime)s traceId=%(traceId)s spanId=%(spanId)s %(message)s') 19 | tracer = Tracer(sampler=AlwaysOnSampler()) 20 | 21 | logger.warning('Before the span') 22 | 23 | with tracer.span(name='hello'): 24 | logger.warning('In the span') 25 | 26 | logger.warning('After the span') -------------------------------------------------------------------------------- /azure_monitor/simple_sample/trace.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from opencensus.ext.azure.log_exporter import AzureLogHandler 5 | 6 | appKey = os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING') 7 | 8 | logger = logging.getLogger(__name__) 9 | logger.addHandler(AzureLogHandler(connection_string=appKey)) 10 | logger.warning('Hello, World!') 11 | 12 | logger.addHandler -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = opencensus-azure-monitor-samples 3 | description = Azure Monitor exporter samples for OpenCensus 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown 6 | author = Microsoft 7 | author_email = appinsightssdk@microsoft.com 8 | url = https://github.com/Azure-Samples/azure-monitor-opencensus-python 9 | platforms = any 10 | license = MIT 11 | classifiers = 12 | Intended Audience :: End Users/Desktop 13 | Development Status :: 5 - Production/Stable 14 | Intended Audience :: End Users/Desktop 15 | License :: OSI Approved :: Apache Software License 16 | Programming Language :: Python 17 | Programming Language :: Python :: 2 18 | Programming Language :: Python :: 2.7 19 | Programming Language :: Python :: 3 20 | Programming Language :: Python :: 3.4 21 | Programming Language :: Python :: 3.5 22 | Programming Language :: Python :: 3.6 23 | Programming Language :: Python :: 3.7 24 | 25 | [options] 26 | package_dir= 27 | =flask_sample 28 | packages=find_namespace: 29 | install_requires = 30 | flask-sqlalchemy >= 2.4.1 31 | flask-wtf >= 0.14.3 32 | opencensus-ext-azure >= 1.0.2 33 | opencensus-ext-flask >= 0.7.3 34 | opencensus-ext-requests >= 0.7.3 35 | opencensus-ext-sqlalchemy >= 0.1.2 36 | 37 | [options.packages.find] 38 | where = flask_sample -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import setuptools 4 | 5 | BASE_DIR = os.path.dirname(__file__) 6 | VERSION_FILENAME = os.path.join(BASE_DIR, "version.py") 7 | PACKAGE_INFO = {} 8 | with open(VERSION_FILENAME) as f: 9 | exec(f.read(), PACKAGE_INFO) 10 | 11 | setuptools.setup(version=PACKAGE_INFO["__version__"]) -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.1.dev0' 2 | 3 | --------------------------------------------------------------------------------