Blog Post

Apps on Azure Blog
6 MIN READ

Part 2 – Observability for your azd-compatible app

pcchan's avatar
pcchan
Icon for Microsoft rankMicrosoft
Nov 06, 2022

In Part 1, I walked you through how to azdev-ify a simple Python app. In this post, we will:

  • add the Azure resources to enable the observability features in azd
  • add manual instrumentation code in the app 
  • create a launch.json file to run the app locally and make sure we can send data to Application Insights
  • deploy the app to Azure

 

Previously…

We azdev-ified a simple Python app: TheCatSaidNo and deployed the app to Azure. Don’t worry if you have already deleted everything. I have updated the code for part 1 because of the Bicep modules improvements we shipped in the azure-dev-cli_0.4.0-beta.1 release. You don't need to update your codes, just start from my GitHub repository (branch: part1):

  1. Make sure have the pre-requisites installed:
  2. In a new empty directory, run 
    azd up -t https://github.com/puicchan/theCatSaidNo -b part1​

    If you run `azd monitor --overview` at this point, you will get an error - “Error: application does not contain an Application Insights dashboard.” That’s because we didn’t create any Azure Monitor resources in part 1,

 

Step 1 - add Application Insights

The Azure Developer CLI (azd) provides a monitor command to help you get insight into how your applications are performing so that you can proactively identify issues. We need to first add the Azure resources to the resource group created in part 1.

  1. Refer to a sample, e.g., ToDo Python Mongo. Copy the directory /infra/core/monitor to your /infra folder.
  2. In main.bicep: add the following parameters. If you want to override the default azd naming convention, provide your own values here. This is new since version 0.4.0-beta.1. 
    param applicationInsightsDashboardName string = ''
    param applicationInsightsName string = ''
    param logAnalyticsName string = ''​
  3. Add the call to monitoring.bicep in /core/monitor
    // Monitor application with Azure Monitor
    module monitoring './core/monitor/monitoring.bicep' = {
      name: 'monitoring'
      scope: rg
      params: {
        location: location
        tags: tags
        logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}'
        applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}'
        applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}'
      }
    }
  4. Pass the application insight name as a param to appservice.bicep in the web module: 
    applicationInsightsName: monitoring.outputs.applicationInsightsName
  5. Add output for the App Insight connection string to make sure it’s stored in the .env file:
    output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString​
  6. Here's the complete main.bicep
    targetScope = 'subscription'
    
    @minLength(1)
    @maxLength(64)
    @description('Name of the the environment which is used to generate a short unique hash used in all resources.')
    param environmentName string
    
    @minLength(1)
    @description('Primary location for all resources')
    param location string
    
    // Optional parameters to override the default azd resource naming conventions. Update the main.parameters.json file to provide values. e.g.,:
    // "resourceGroupName": {
    //      "value": "myGroupName"
    // }
    param appServicePlanName string = ''
    param resourceGroupName string = ''
    param webServiceName string = ''
    param applicationInsightsDashboardName string = ''
    param applicationInsightsName string = ''
    param logAnalyticsName string = ''
    // serviceName is used as value for the tag (azd-service-name) azd uses to identify
    param serviceName string = 'web'
    
    @description('Id of the user or app to assign application roles')
    param principalId string = ''
    
    var abbrs = loadJsonContent('./abbreviations.json')
    var resourceToken = toLower(uniqueString(subscription().id, environmentName, location))
    var tags = { 'azd-env-name': environmentName }
    
    // Organize resources in a resource group
    resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
      name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}'
      location: location
      tags: tags
    }
    
    // The application frontend
    module web './core/host/appservice.bicep' = {
      name: serviceName
      scope: rg
      params: {
        name: !empty(webServiceName) ? webServiceName : '${abbrs.webSitesAppService}web-${resourceToken}'
        location: location
        tags: union(tags, { 'azd-service-name': serviceName })
        applicationInsightsName: monitoring.outputs.applicationInsightsName
        appServicePlanId: appServicePlan.outputs.id
        runtimeName: 'python'
        runtimeVersion: '3.8'
        scmDoBuildDuringDeployment: true
      }
    }
    
    // Create an App Service Plan to group applications under the same payment plan and SKU
    module appServicePlan './core/host/appserviceplan.bicep' = {
      name: 'appserviceplan'
      scope: rg
      params: {
        name: !empty(appServicePlanName) ? appServicePlanName : '${abbrs.webServerFarms}${resourceToken}'
        location: location
        tags: tags
        sku: {
          name: 'B1'
        }
      }
    }
    
    // Monitor application with Azure Monitor
    module monitoring './core/monitor/monitoring.bicep' = {
      name: 'monitoring'
      scope: rg
      params: {
        location: location
        tags: tags
        logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}'
        applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}'
        applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}'
      }
    }
    
    // App outputs
    output AZURE_LOCATION string = location
    output AZURE_TENANT_ID string = tenant().tenantId
    output REACT_APP_WEB_BASE_URL string = web.outputs.uri
    output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString
  7. Run `azd provision` to provision the additional Azure resources
  8. Once provisioning is complete, run `azd monitor --overview` to open the Application Insight dashboard in the browser.

    The dashboard is not that exciting yet. Auto-instrumentation application monitoring is not yet available for Python appHowever, if you examine your code, you will see that:

    • APPLICATIONINSIGHTS_CONNECTION_STRING is added to the .env file for your current azd environment.
    • The same connection string is added to the application settings in the configuration of your web app in Azure Portal:

 

Step 2 - manually instrumenting your app

Let’s track incoming requests with OpenCensus Python and instrument your application with the flask middleware so that incoming requests sent to your app is tracked. (To learn more about what Azure Monitor supports, refer to setting up Azure Monitor for your Python app.)

 

For this step, I recommend using Visual Studio Code and the following extensions:

Get Started Tutorial for Python in Visual Studio Code is a good reference if you are not familiar with Visual Studio Code.

 

  1. Add to requirements.txt
    python-dotenv
    opencensus-ext-azure >= 1.0.2
    opencensus-ext-flask >= 0.7.3
    opencensus-ext-requests >= 0.7.3​
  2. Modify app.py to: 
    import os
    
    from dotenv import load_dotenv
    from flask import Flask, render_template, send_from_directory
    from opencensus.ext.azure.trace_exporter import AzureExporter
    from opencensus.ext.flask.flask_middleware import FlaskMiddleware
    from opencensus.trace.samplers import ProbabilitySampler
    
    INSTRUMENTATION_KEY = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
    
    app = Flask(__name__)
    middleware = FlaskMiddleware(
        app,
        exporter=AzureExporter(connection_string=INSTRUMENTATION_KEY),
        sampler=ProbabilitySampler(rate=1.0),
    )
    
    
    @app.route("/favicon.ico")
    def favicon():
        return send_from_directory(
            os.path.join(app.root_path, "static"),
            "favicon.ico",
            mimetype="image/vnd.microsoft.icon",
        )
    
    
    @app.route("/")
    def home():
        return render_template("home.html")
    
    
    if __name__ == "__main__":
        app.run(debug=True)​
  3. To run locally, we need to read from the .env file to get the current azd environment context. The easiest is to customize run and debug in Visual Studio Code by creating a launch.json file:
    • Ctrl-Shift+D or click “Run and Debug” in the sidebar
    • Click “create a launch.json file” to customize a launch.json file
    • Select “Flask Launch and debug a Flask web application
    • Modify the generated file to: 
      {
          // Use IntelliSense to learn about possible attributes.
          // Hover to view descriptions of existing attributes.
          // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
          "version": "0.2.0",
          "configurations": [
              {
                  "name": "Python: Flask",
                  "type": "python",
                  "request": "launch",
                  "module": "flask",
                  "env": {
                      "FLASK_APP": "app.py",
                      "FLASK_DEBUG": "1"
                  },
                  "args": [
                      "run",
                      "--no-debugger",
                      "--no-reload"
                  ],
                  "jinja": true,
                  "justMyCode": true,
                  "envFile": "${input:dotEnvFilePath}"
              }
          ],
          "inputs": [
              {
                  "id": "dotEnvFilePath",
                  "type": "command",
                  "command": "azure-dev.commands.getDotEnvFilePath"
              }
          ]
      }​
  4. Create and activate a new virtual environment . I am using Windows. So: 
    py -m venv .venv
    .venv\scripts\activate
    pip3 install -r ./requirements.txt​
  5. Click the Run view in the sidebar and hit the play button for Python: Flask
    • Browse to http://localhost:5000 to launch the app.
    • Click the button a few times and/or reload the page to generate some traffic.

    Take a break; perhaps play with your cat or dog for real. The data will take a short while to show up in Application Insights.

  6. Run `azd monitor --overview` to open the dashboard and notice the change 
  7. Run `azd deploy` to deploy your app to Azure and start monitoring your app!

 

Get the code for this blog post here. Next, we will explore how you can use `azd pipeline config` to set up GitHub action to deploy update upon code check in.

 

Feel free to run `azd down` to clean up all the Azure resources. As you saw, it’s easy to get things up and running again. Just `azd up`!

 

We love your feedback! If you have any comments or ideas, feel free to add a comment or submit an issue to the Azure Developer CLI Repo.

Updated Nov 06, 2022
Version 1.0
No CommentsBe the first to comment