From ea5a3c97241365775843ba9d8279b1a9ff8c26ae Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 13 Feb 2025 16:23:31 -0800 Subject: [PATCH 1/6] Adds quickstarts --- MinimalQuickstarts/aspnet_telemetry.md | 42 ++++++++++++++++++ MinimalQuickstarts/python.md | 60 ++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 MinimalQuickstarts/aspnet_telemetry.md create mode 100644 MinimalQuickstarts/python.md diff --git a/MinimalQuickstarts/aspnet_telemetry.md b/MinimalQuickstarts/aspnet_telemetry.md new file mode 100644 index 0000000..cfd4b15 --- /dev/null +++ b/MinimalQuickstarts/aspnet_telemetry.md @@ -0,0 +1,42 @@ +# ASP.NET + +## Setup Azure Monitor/App Insights + +[Setup Application Insights](https://learn.microsoft.com/en-us/azure/azure-monitor/app/asp-net-core) to instrument telemetry into your application. + +```dotnet +builder.Services.AddApplicationInsightsTelemetry(); +``` + +## Setup App Configuration + +[Connect your application to App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-aspnet-core-app?tabs=entra-id), which supplies feature flags and other configurations to your application. +```dotnet +builder.Configuration.AddAzureAppConfiguration(options => +{ + options.Connect(new Uri(endpoint), new DefaultAzureCredential()); +}); +``` + +## Setup Feature Management + +Add Feature Management to the service collection and add the targeting middleware. [Learn More](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-feature-flag-aspnet-core#use-a-feature-flag) + +```dotnet +builder.Services.AddFeatureManagement() + .WithTargeting() + .AddApplicationInsightsTelemetry(); + +app.UseMiddleware(); +``` + +## Use Feature Management + +Use DI to retrieve the FeatureManager, and get the assigned variant of the feature flag for the user. Use DI to get an instance of `FeatureManager` for flag data and `TelemetryClient` for custom events. + +```dotnet +Variant variant = await _featureManager + .GetVariantAsync("MyFeatureFlag", HttpContext.RequestAborted); + +telemetryClient.TrackEvent("checkout"); +``` \ No newline at end of file diff --git a/MinimalQuickstarts/python.md b/MinimalQuickstarts/python.md new file mode 100644 index 0000000..59ce25b --- /dev/null +++ b/MinimalQuickstarts/python.md @@ -0,0 +1,60 @@ +# Python + +## Define a targeting context accessor method + +The accessor method is called from both FeatureManagement and OpenTelemetry to identify who or what the current context is. + +```python +async def my_targeting_accessor() -> TargetingContext: + session_id = "" + + if "Session-ID" in request.headers: + session_id = request.headers["Session-ID"] + + return TargetingContext(user_id=session_id) +``` + +## Setup Azure Monitor/App Insights + +Use one of the [Azure Monitor OpenTelemetry libraries](https://learn.microsoft.com/python/api/overview/azure/monitor-opentelemetry-readme?view=azure-python#officially-supported-instrumentations) to instrument telemetry into your application. Include the TargetingSpanProcessor if you want request and dependency data to be available for metrics. + +```python +from azure.monitor.opentelemetry import configure_azure_monitor +from featuremanagement.azuremonitor import TargetingSpanProcessor + +# Configure Azure Monitor +configure_azure_monitor( + connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"), + span_processors=[TargetingSpanProcessor(targeting_context_accessor=my_targeting_accessor)], +) +``` + +## Setup App Configuration + +[Connect your application to App Configuration](https://learn.microsoft.com/azure/azure-app-configuration/quickstart-feature-flag-python#console-applications), which supplies feature flags and other configurations to your application. + +```python +from azure.appconfiguration.provider import load +from azure.identity import DefaultAzureCredential + +azure_app_config = load(endpoint="", credential=DefaultAzureCredential(), feature_flag_enabled=True, feature_flag_refresh_enabled=True)) +``` + +## Use Feature Management + +Finally, use the [FeatureManagement library](https://learn.microsoft.comazure/azure-app-configuration/quickstart-feature-flag-python) with telemetry enabled to evaluate the flag for the given user. + +```python +from featuremanagement import FeatureManager +from featuremanagement.azuremonitor import publish_telemetry, track_event + +feature_manager = FeatureManager(azure_app_config, on_feature_evaluated=publish_telemetry, targeting_context_accessor=my_targeting_accessor) + +if feature_manager.get_variant("My Feature"): + # Feature is on +else: + # Feature is off + +# Track a custom event (the user_id from the targeting context needs to be manually added here for now) +track_event("Like", my_targeting_accessor().user_id) +``` From 139de3c0d05a59055edec68b45dc51c59da2e1c3 Mon Sep 17 00:00:00 2001 From: Ross Grambo Date: Thu, 13 Feb 2025 16:44:23 -0800 Subject: [PATCH 2/6] Rename --- .../{aspnet_telemetry.md => aspnet_app_insights.md} | 0 MinimalQuickstarts/{python.md => python_otel.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename MinimalQuickstarts/{aspnet_telemetry.md => aspnet_app_insights.md} (100%) rename MinimalQuickstarts/{python.md => python_otel.md} (100%) diff --git a/MinimalQuickstarts/aspnet_telemetry.md b/MinimalQuickstarts/aspnet_app_insights.md similarity index 100% rename from MinimalQuickstarts/aspnet_telemetry.md rename to MinimalQuickstarts/aspnet_app_insights.md diff --git a/MinimalQuickstarts/python.md b/MinimalQuickstarts/python_otel.md similarity index 100% rename from MinimalQuickstarts/python.md rename to MinimalQuickstarts/python_otel.md From c5761d3afda78661fe93ba95b75cd8215677b3be Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 20 Feb 2025 15:45:15 -0800 Subject: [PATCH 3/6] Flask Sample --- MinimalQuickstarts/python_flask.py | 66 ++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 MinimalQuickstarts/python_flask.py diff --git a/MinimalQuickstarts/python_flask.py b/MinimalQuickstarts/python_flask.py new file mode 100644 index 0000000..ca9dde1 --- /dev/null +++ b/MinimalQuickstarts/python_flask.py @@ -0,0 +1,66 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- +import os +import uuid +from azure.appconfiguration.provider import load +from azure.identity import DefaultAzureCredential +from azure.monitor.opentelemetry import configure_azure_monitor +from featuremanagement import FeatureManager, TargetingContext +from featuremanagement.azuremonitor import TargetingSpanProcessor, track_event, publish_telemetry +from opentelemetry import trace +from opentelemetry.trace import get_tracer_provider +from opentelemetry.baggage import set_baggage, get_baggage + +def my_targeting_accessor() -> TargetingContext: + session_id = get_baggage("Session-ID") + if not session_id: + session_id = str(uuid.uuid4()) + set_baggage("Session-ID", session_id) + set_baggage("Groups", ["Beta, Alpha"]) + return TargetingContext(user_id=session_id) + +# Configure Azure Monitor +configure_azure_monitor( + connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"), + span_processors=[TargetingSpanProcessor(targeting_context_accessor=my_targeting_accessor)], +) + +tracer = trace.get_tracer(__name__, tracer_provider=get_tracer_provider()) + +from flask import Flask, make_response, session, request +from flask.sessions import SecureCookieSessionInterface + +app = Flask(__name__) +app.session_interface = SecureCookieSessionInterface() +app.secret_key = os.urandom(24) + +ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") +credential = DefaultAzureCredential() + +global azure_app_config, feature_manager +azure_app_config = load( + endpoint=ENDPOINT, + credential=credential, + feature_flag_enabled=True, + feature_flag_refresh_enabled=True, +) + +feature_manager = FeatureManager(azure_app_config, targeting_context_accessor=my_targeting_accessor, on_feature_evaluated=publish_telemetry) +app.config.update(azure_app_config) + +@app.route("/") +def index(): + global azure_app_config + # Refresh the configuration from App Configuration service. + azure_app_config.refresh() + response = make_response(str(feature_manager.is_enabled("Beta"))) + response.mimetype = "text/plain" + track_event("index", get_baggage("Session-ID")) + + return response + +if __name__ == "__main__": + app.run() \ No newline at end of file From d08ac6d9da37bae9ad7d05f5149bcfb9d27beb53 Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 20 Feb 2025 15:46:50 -0800 Subject: [PATCH 4/6] Update python_flask.py --- MinimalQuickstarts/python_flask.py | 1 - 1 file changed, 1 deletion(-) diff --git a/MinimalQuickstarts/python_flask.py b/MinimalQuickstarts/python_flask.py index ca9dde1..4961600 100644 --- a/MinimalQuickstarts/python_flask.py +++ b/MinimalQuickstarts/python_flask.py @@ -19,7 +19,6 @@ def my_targeting_accessor() -> TargetingContext: if not session_id: session_id = str(uuid.uuid4()) set_baggage("Session-ID", session_id) - set_baggage("Groups", ["Beta, Alpha"]) return TargetingContext(user_id=session_id) # Configure Azure Monitor From 29c1ff596ea5583bc3a148826e513d959196830b Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Thu, 20 Feb 2025 15:47:01 -0800 Subject: [PATCH 5/6] Create python_quart.py --- MinimalQuickstarts/python_quart.py | 54 ++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 MinimalQuickstarts/python_quart.py diff --git a/MinimalQuickstarts/python_quart.py b/MinimalQuickstarts/python_quart.py new file mode 100644 index 0000000..ddb0a22 --- /dev/null +++ b/MinimalQuickstarts/python_quart.py @@ -0,0 +1,54 @@ +# ------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# ------------------------------------------------------------------------- + +import uuid +import os +from quart import Quart, session +from quart.sessions import SecureCookieSessionInterface +from azure.appconfiguration.provider import load +from azure.identity import DefaultAzureCredential +from azure.monitor.opentelemetry import configure_azure_monitor +from featuremanagement.aio import FeatureManager +from featuremanagement import TargetingContext +from featuremanagement.azuremonitor import TargetingSpanProcessor, track_event +from opentelemetry.baggage import set_baggage, get_baggage + + +# A callback for assigning a TargetingContext for both Telemetry logs and Feature Flag evaluation +async def my_targeting_accessor() -> TargetingContext: + session_id = get_baggage("Session-ID") + if not session_id: + session_id = str(uuid.uuid4()) + set_baggage("Session-ID", session_id) + return TargetingContext(user_id=session_id) + + +# Configure Azure Monitor +configure_azure_monitor( + connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"), + span_processors=[TargetingSpanProcessor(targeting_context_accessor=my_targeting_accessor)], +) + +app = Quart(__name__) +app.session_interface = SecureCookieSessionInterface() +app.secret_key = os.urandom(24) + +endpoint = os.environ.get("APPCONFIGURATION_ENDPOINT_STRING") +credential = DefaultAzureCredential() + +# Connecting to Azure App Configuration using AAD +config = load(endpoint=endpoint, credential=credential, feature_flag_enabled=True, feature_flag_refresh_enabled=True) + +# Load feature flags and set up targeting context accessor +feature_manager = FeatureManager(config, targeting_context_accessor=my_targeting_accessor) + +@app.route("/") +async def hello(): + track_event("index", session["Session-ID"]) + return str(feature_manager.is_enabled("Beta")) + + +app.run() From f00de3ecb5e7e4ad77b67ca0ddec3ea378eb78ce Mon Sep 17 00:00:00 2001 From: Matt Metcalf Date: Mon, 24 Feb 2025 13:38:14 -0800 Subject: [PATCH 6/6] Django --- .gitignore | 3 +- MinimalQuickstarts/python-django/README.md | 77 ++++++++++++++++++ MinimalQuickstarts/python-django/manage.py | 16 ++++ .../quickstartproject/__init__.py | 0 .../python-django/quickstartproject/asgi.py | 7 ++ .../quickstartproject/middleware.py | 17 ++++ .../quickstartproject/settings.py | 78 +++++++++++++++++++ .../python-django/quickstartproject/urls.py | 18 +++++ .../python-django/quickstartproject/wsgi.py | 8 ++ .../python-django/requirements.txt | 4 + 10 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 MinimalQuickstarts/python-django/README.md create mode 100644 MinimalQuickstarts/python-django/manage.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/__init__.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/asgi.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/middleware.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/settings.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/urls.py create mode 100644 MinimalQuickstarts/python-django/quickstartproject/wsgi.py create mode 100644 MinimalQuickstarts/python-django/requirements.txt diff --git a/.gitignore b/.gitignore index 633122a..baa01c7 100644 --- a/.gitignore +++ b/.gitignore @@ -413,4 +413,5 @@ package-lock.json # JavaScript bundler folder out/ -*.tgz \ No newline at end of file +*.tgz +MinimalQuickstarts/python-django/db.sqlite3 diff --git a/MinimalQuickstarts/python-django/README.md b/MinimalQuickstarts/python-django/README.md new file mode 100644 index 0000000..9c1053c --- /dev/null +++ b/MinimalQuickstarts/python-django/README.md @@ -0,0 +1,77 @@ +# Sample Python Django Application using Azure App Configuration + +This is the sample Django application that uses the Azure App Configuration Service [Deploy a Python (Django or Flask) web app to Azure App Service](https://docs.microsoft.com/en-us/azure/app-service/quickstart-python). For instructions on how to create the Azure resources and deploy the application to Azure, refer to the Quickstart article. + +A [sample Flask application](../python-flask-webapp-sample/) is also available. + +If you need an Azure account, you can [create one for free](https://azure.microsoft.com/en-us/free/). + +## Prerequisites + +You must have an [Azure subscription][azure_sub], and a [Configuration Store][configuration_store] to use this package. + +To create a Configuration Store, you can either use [Azure Portal](https://ms.portal.azure.com/#create/Microsoft.Azconfig) or if you are using [Azure CLI][azure_cli] you can simply run the following snippet in your console: + +```Powershell +az appconfig create --name --resource-group --location eastus +``` + +### Create Keys + +```Powershell +az appconfig kv set --name --key testapp_settings_message --value "Hello from Azure App Configuration" +az appconfig feature set --name --feature Beta +``` + +## Setup + +Install the Azure App Configuration Provider client library for Python and other dependencies with pip: + +```commandline +pip install -r requirements.txt +``` + +Set your App Configuration store endpoint as an environment variable. + +### Command Prompt + +```commandline +setx AZURE_APPCONFIG_ENDPOINT "your-store-endpoint" +``` + +### PowerShell + +```Powershell +$Env:AZURE_APPCONFIG_ENDPOINT="your-store-endpoint" +``` + +### Linux/ MacOS + +```Bash +export AZURE_APPCONFIG_ENDPOINT="your-store-enpoint" +``` + +Start the django application using the following command: +```commandline +# Run database migration +python manage.py migrate +# Run the app at http://127.0.0.1:8000 +python manage.py runserver +``` + +## Refresh Configuration + +To refresh your configuration, you first update the value in Azure App Configuration, then update the Sentinel value to trigger a refresh. + +```Powershell +az appconfig feature enable --name --feature Beta +``` + +Refresh the page in your browser to see the updated value. + +NOTE: By default refresh can only be triggered every 30 seconds. You might have to wait up to 30 seconds and refresh the page again in order to see a change. + + +[azure_sub]: https://azure.microsoft.com/free/ +[azure_cli]: https://docs.microsoft.com/cli/azure +[configuration_store]: https://azure.microsoft.com/services/app-configuration/ diff --git a/MinimalQuickstarts/python-django/manage.py b/MinimalQuickstarts/python-django/manage.py new file mode 100644 index 0000000..9c4e14c --- /dev/null +++ b/MinimalQuickstarts/python-django/manage.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys +from django.core.management import execute_from_command_line +from opentelemetry.instrumentation.django import DjangoInstrumentor + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quickstartproject.settings') + DjangoInstrumentor().instrument() + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/MinimalQuickstarts/python-django/quickstartproject/__init__.py b/MinimalQuickstarts/python-django/quickstartproject/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/MinimalQuickstarts/python-django/quickstartproject/asgi.py b/MinimalQuickstarts/python-django/quickstartproject/asgi.py new file mode 100644 index 0000000..af77011 --- /dev/null +++ b/MinimalQuickstarts/python-django/quickstartproject/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'quickstartproject.settings') + +application = get_asgi_application() diff --git a/MinimalQuickstarts/python-django/quickstartproject/middleware.py b/MinimalQuickstarts/python-django/quickstartproject/middleware.py new file mode 100644 index 0000000..da0b1ec --- /dev/null +++ b/MinimalQuickstarts/python-django/quickstartproject/middleware.py @@ -0,0 +1,17 @@ +from opentelemetry.baggage import set_baggage +from opentelemetry.context import attach +from opentelemetry.trace import get_current_span + +class SimpleMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + attach(set_baggage("Session-ID", request.session.session_key)) + attach(set_baggage("Groups", ["Beta, Alpha"])) + + get_current_span().set_attribute("TargetingId", request.session.session_key) + + response = self.get_response(request) + + return response \ No newline at end of file diff --git a/MinimalQuickstarts/python-django/quickstartproject/settings.py b/MinimalQuickstarts/python-django/quickstartproject/settings.py new file mode 100644 index 0000000..efdb8e4 --- /dev/null +++ b/MinimalQuickstarts/python-django/quickstartproject/settings.py @@ -0,0 +1,78 @@ +import os +from azure.appconfiguration.provider import load, WatchKey +from azure.identity import DefaultAzureCredential +from azure.monitor.opentelemetry import configure_azure_monitor +from featuremanagement import FeatureManager, TargetingContext +from featuremanagement.azuremonitor import TargetingSpanProcessor, publish_telemetry +from opentelemetry.baggage import get_baggage +from pathlib import Path + +ALLOWED_HOSTS = [] + + +def my_targeting_accessor() -> TargetingContext: + return TargetingContext(user_id=get_baggage("Session-ID")) + +# Configure Azure Monitor +configure_azure_monitor( + connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING"), + span_processors=[TargetingSpanProcessor(targeting_context_accessor=my_targeting_accessor)], +) + +CONFIG = {} + +ENDPOINT = os.environ.get("AZURE_APPCONFIG_ENDPOINT") + +# Set up credentials and settings used in resolving key vault references. +credential = DefaultAzureCredential() + +def callback(): + global AZURE_APP_CONFIG + # Update Django settings with the app configuration key-values + CONFIG.update(AZURE_APP_CONFIG) + +# Load app configuration key-values +AZURE_APP_CONFIG = load( + endpoint=ENDPOINT, + credential=credential, + refresh_on=[WatchKey("sentinel")], + feature_flag_enabled=True, + feature_flag_refresh_enabled=True, + on_refresh_success=callback, +) + +FEATURE_MANAGER = FeatureManager(AZURE_APP_CONFIG, targeting_context_accessor=my_targeting_accessor, on_feature_evaluated=publish_telemetry) + + +# Updates the config object with the app configuration key-values and resolved key vault reference values. +# This will override any values in the config object with the same key. +CONFIG.update(AZURE_APP_CONFIG) + +SECRET_KEY = "a" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ROOT_URLCONF = "quickstartproject.urls" + +MIDDLEWARE = [ +'django.contrib.sessions.middleware.SessionMiddleware', +'quickstartproject.middleware.SimpleMiddleware', +] + +INSTALLED_APPS = [ + "django.contrib.sessions", +] + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + diff --git a/MinimalQuickstarts/python-django/quickstartproject/urls.py b/MinimalQuickstarts/python-django/quickstartproject/urls.py new file mode 100644 index 0000000..a9488fb --- /dev/null +++ b/MinimalQuickstarts/python-django/quickstartproject/urls.py @@ -0,0 +1,18 @@ +from django.urls import path +from django.http import HttpResponse +from django.conf import settings +from opentelemetry.baggage import get_baggage +from featuremanagement.azuremonitor import track_event + + +async def index(request): + # Refresh the configuration from App Configuration service. + settings.AZURE_APP_CONFIG.refresh() + track_event("index", request.session.session_key) + + return HttpResponse(str(settings.FEATURE_MANAGER.is_enabled("Beta"))) + + +urlpatterns = [ + path('', index, name='index'), +] diff --git a/MinimalQuickstarts/python-django/quickstartproject/wsgi.py b/MinimalQuickstarts/python-django/quickstartproject/wsgi.py new file mode 100644 index 0000000..9accbcc --- /dev/null +++ b/MinimalQuickstarts/python-django/quickstartproject/wsgi.py @@ -0,0 +1,8 @@ +import os + +from django.core.wsgi import get_wsgi_application + +settings_module = 'quickstartproject.settings' +os.environ.setdefault('DJANGO_SETTINGS_MODULE', settings_module) + +application = get_wsgi_application() diff --git a/MinimalQuickstarts/python-django/requirements.txt b/MinimalQuickstarts/python-django/requirements.txt new file mode 100644 index 0000000..c463dee --- /dev/null +++ b/MinimalQuickstarts/python-django/requirements.txt @@ -0,0 +1,4 @@ +Django==4.2.16 +azure-appconfiguration-provider==1.3.0 +azure-identity==1.16.1 +featuremanagement==1.0.0