⏱️ Estimated Time: 40-50 minutes | 💰 Cost Impact: ~$5-15/month | ⭐ Complexity: Intermediate
📚 Learning Path:
- ← Previous: Preflight Checks - Pre-deployment validation
- 🎯 You Are Here: Application Insights Integration (Monitoring, telemetry, debugging)
- → Next: Deployment Guide - Deploy to Azure
- 🏠 Course Home
By completing this lesson, you will:
- Integrate Application Insights into AZD projects automatically
- Configure distributed tracing for microservices
- Implement custom telemetry (metrics, events, dependencies)
- Set up live metrics for real-time monitoring
- Create alerts and dashboards from AZD deployments
- Debug production issues with telemetry queries
- Optimize costs and sampling strategies
- Monitor AI/LLM applications (tokens, latency, costs)
Without Application Insights:
❌ No visibility into production behavior
❌ Manual log aggregation across services
❌ Reactive debugging (wait for customer complaints)
❌ No performance metrics
❌ Cannot trace requests across services
❌ Unknown failure rates and bottlenecks
With Application Insights + AZD:
✅ Automatic telemetry collection
✅ Centralized logs from all services
✅ Proactive issue detection
✅ End-to-end request tracing
✅ Performance metrics and insights
✅ Real-time dashboards
✅ AZD provisions everything automatically
Analogy: Application Insights is like having a "black box" flight recorder + cockpit dashboard for your application. You see everything that's happening in real-time and can replay any incident.
graph TB
User[User/Client]
App1[Container App 1<br/>API Gateway]
App2[Container App 2<br/>Product Service]
App3[Container App 3<br/>Order Service]
AppInsights[Application Insights<br/>Telemetry Hub]
LogAnalytics[(Log Analytics<br/>Workspace)]
Portal[Azure Portal<br/>Dashboards & Alerts]
Query[Kusto Queries<br/>Custom Analysis]
User --> App1
App1 --> App2
App2 --> App3
App1 -.->|Auto-instrumentation| AppInsights
App2 -.->|Auto-instrumentation| AppInsights
App3 -.->|Auto-instrumentation| AppInsights
AppInsights --> LogAnalytics
LogAnalytics --> Portal
LogAnalytics --> Query
style AppInsights fill:#9C27B0,stroke:#7B1FA2,stroke-width:3px,color:#fff
style LogAnalytics fill:#4CAF50,stroke:#388E3C,stroke-width:3px,color:#fff
| Telemetry Type | What It Captures | Use Case |
|---|---|---|
| Requests | HTTP requests, status codes, duration | API performance monitoring |
| Dependencies | External calls (DB, APIs, storage) | Identify bottlenecks |
| Exceptions | Unhandled errors with stack traces | Debugging failures |
| Custom Events | Business events (signup, purchase) | Analytics and funnels |
| Metrics | Performance counters, custom metrics | Capacity planning |
| Traces | Log messages with severity | Debugging and auditing |
| Availability | Uptime and response time tests | SLA monitoring |
# Verify Azure Developer CLI
azd version
# ✅ Expected: azd version 1.0.0 or higher
# Verify Azure CLI
az --version
# ✅ Expected: azure-cli 2.50.0 or higher- Active Azure subscription
- Permissions to create:
- Application Insights resources
- Log Analytics workspaces
- Container Apps
- Resource groups
You should have completed:
- AZD Basics - Core AZD concepts
- Configuration - Environment setup
- First Project - Basic deployment
AZD automatically creates and configures Application Insights when you deploy. Let's see how it works.
monitored-app/
├── azure.yaml # AZD configuration
├── infra/
│ ├── main.bicep # Main infrastructure
│ ├── core/
│ │ └── monitoring.bicep # Application Insights + Log Analytics
│ └── app/
│ └── api.bicep # Container App with monitoring
└── src/
├── app.py # Application with telemetry
├── requirements.txt
└── Dockerfile
File: azure.yaml
name: monitored-app
metadata:
template: monitored-app@1.0.0
services:
api:
project: ./src
language: python
host: containerapp
# AZD automatically provisions monitoring!That's it! AZD will create Application Insights by default. No extra configuration needed for basic monitoring.
File: infra/core/monitoring.bicep
param logAnalyticsName string
param applicationInsightsName string
param location string = resourceGroup().location
param tags object = {}
// Log Analytics Workspace (required for Application Insights)
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: logAnalyticsName
location: location
tags: tags
properties: {
sku: {
name: 'PerGB2018' // Pay-as-you-go pricing
}
retentionInDays: 30 // Keep logs for 30 days
features: {
enableLogAccessUsingOnlyResourcePermissions: true
}
}
}
// Application Insights
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: applicationInsightsName
location: location
tags: tags
kind: 'web'
properties: {
Application_Type: 'web'
WorkspaceResourceId: logAnalytics.id
IngestionMode: 'LogAnalytics'
publicNetworkAccessForIngestion: 'Enabled'
publicNetworkAccessForQuery: 'Enabled'
}
}
// Outputs for Container Apps
output logAnalyticsWorkspaceId string = logAnalytics.id
output logAnalyticsWorkspaceName string = logAnalytics.name
output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString
output applicationInsightsInstrumentationKey string = applicationInsights.properties.InstrumentationKey
output applicationInsightsName string = applicationInsights.nameFile: infra/app/api.bicep
param name string
param location string
param tags object = {}
param containerAppsEnvironmentName string
param applicationInsightsConnectionString string
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: name
location: location
tags: tags
properties: {
configuration: {
ingress: {
external: true
targetPort: 8000
}
secrets: [
{
name: 'appinsights-connection-string'
value: applicationInsightsConnectionString
}
]
}
template: {
containers: [
{
name: 'api'
image: 'myregistry.azurecr.io/api:latest'
resources: {
cpu: json('0.5')
memory: '1Gi'
}
env: [
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
secretRef: 'appinsights-connection-string'
}
{
name: 'APPLICATIONINSIGHTS_ENABLED'
value: 'true'
}
]
}
]
}
}
}
output uri string = 'https://${containerApp.properties.configuration.ingress.fqdn}'File: src/app.py
from flask import Flask, request, jsonify
from opencensus.ext.azure.log_exporter import AzureLogHandler
from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.ext.flask.flask_middleware import FlaskMiddleware
from opencensus.trace.samplers import ProbabilitySampler
import logging
import os
app = Flask(__name__)
# Get Application Insights connection string
connection_string = os.environ.get('APPLICATIONINSIGHTS_CONNECTION_STRING')
if connection_string:
# Configure distributed tracing
middleware = FlaskMiddleware(
app,
exporter=AzureExporter(connection_string=connection_string),
sampler=ProbabilitySampler(rate=1.0) # 100% sampling for dev
)
# Configure logging
logger = logging.getLogger(__name__)
logger.addHandler(AzureLogHandler(connection_string=connection_string))
logger.setLevel(logging.INFO)
print("✅ Application Insights enabled")
else:
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
print("⚠️ Application Insights not configured")
@app.route('/health')
def health():
logger.info('Health check endpoint called')
return jsonify({'status': 'healthy', 'monitoring': 'enabled'})
@app.route('/api/products')
def get_products():
logger.info('Fetching products')
# Simulate database call (automatically tracked as dependency)
products = [
{'id': 1, 'name': 'Laptop', 'price': 999.99},
{'id': 2, 'name': 'Mouse', 'price': 29.99},
{'id': 3, 'name': 'Keyboard', 'price': 79.99}
]
logger.info(f'Returned {len(products)} products')
return jsonify(products)
@app.route('/api/error-test')
def error_test():
"""Test error tracking"""
logger.error('Testing error tracking')
try:
raise ValueError('This is a test exception')
except Exception as e:
logger.exception('Exception occurred in error-test endpoint')
return jsonify({'error': str(e)}), 500
@app.route('/api/slow')
def slow_endpoint():
"""Test performance tracking"""
import time
logger.info('Slow endpoint called')
time.sleep(3) # Simulate slow operation
logger.warning('Endpoint took 3 seconds to respond')
return jsonify({'message': 'Slow operation completed'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)File: src/requirements.txt
Flask==3.0.0
opencensus-ext-azure==1.1.13
opencensus-ext-flask==0.8.1
gunicorn==21.2.0# Initialize AZD
azd init
# Deploy (provisions Application Insights automatically)
azd up
# Get app URL
APP_URL=$(azd env get-values | grep API_URL | cut -d '=' -f2 | tr -d '"')
# Generate telemetry
curl $APP_URL/health
curl $APP_URL/api/products
curl $APP_URL/api/error-test
curl $APP_URL/api/slow✅ Expected output:
{
"status": "healthy",
"monitoring": "enabled"
}# Get Application Insights details
azd env get-values | grep APPLICATIONINSIGHTS
# Open in Azure Portal
az monitor app-insights component show \
--app $(azd env get-values | grep APPLICATIONINSIGHTS_NAME | cut -d '=' -f2 | tr -d '"') \
--resource-group $(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"') \
--query "appId" -o tsvNavigate to Azure Portal → Application Insights → Transaction Search
You should see:
- ✅ HTTP requests with status codes
- ✅ Request duration (3+ seconds for
/api/slow) - ✅ Exception details from
/api/error-test - ✅ Custom log messages
Let's add custom telemetry for business-critical events.
File: src/telemetry.py
from opencensus.ext.azure import metrics_exporter
from opencensus.stats import aggregation as aggregation_module
from opencensus.stats import measure as measure_module
from opencensus.stats import stats as stats_module
from opencensus.stats import view as view_module
from opencensus.tags import tag_map as tag_map_module
from opencensus.ext.azure.log_exporter import AzureLogHandler
from opencensus.ext.azure.trace_exporter import AzureExporter
from opencensus.trace import tracer as tracer_module
import logging
import os
class TelemetryClient:
"""Custom telemetry client for Application Insights"""
def __init__(self, connection_string=None):
self.connection_string = connection_string or os.environ.get('APPLICATIONINSIGHTS_CONNECTION_STRING')
if not self.connection_string:
print("⚠️ Application Insights connection string not found")
return
# Setup logger
self.logger = logging.getLogger(__name__)
self.logger.addHandler(AzureLogHandler(connection_string=self.connection_string))
self.logger.setLevel(logging.INFO)
# Setup metrics exporter
self.stats = stats_module.stats
self.view_manager = self.stats.view_manager
self.stats_recorder = self.stats.stats_recorder
exporter = metrics_exporter.new_metrics_exporter(
connection_string=self.connection_string
)
self.view_manager.register_exporter(exporter)
# Setup tracer
self.tracer = tracer_module.Tracer(
exporter=AzureExporter(connection_string=self.connection_string)
)
print("✅ Custom telemetry client initialized")
def track_event(self, event_name: str, properties: dict = None):
"""Track custom business event"""
properties = properties or {}
self.logger.info(
f"CustomEvent: {event_name}",
extra={
'custom_dimensions': {
'event_name': event_name,
**properties
}
}
)
def track_metric(self, metric_name: str, value: float, properties: dict = None):
"""Track custom metric"""
properties = properties or {}
self.logger.info(
f"CustomMetric: {metric_name} = {value}",
extra={
'custom_dimensions': {
'metric_name': metric_name,
'value': value,
**properties
}
}
)
def track_dependency(self, name: str, dependency_type: str, duration: float, success: bool):
"""Track external dependency call"""
with self.tracer.span(name=name) as span:
span.add_attribute('dependency.type', dependency_type)
span.add_attribute('duration', duration)
span.add_attribute('success', success)
# Global telemetry client
telemetry = TelemetryClient()File: src/app.py (enhanced)
from flask import Flask, request, jsonify
from telemetry import telemetry
import time
import random
app = Flask(__name__)
@app.route('/api/purchase', methods=['POST'])
def purchase():
"""Track purchase event with custom telemetry"""
data = request.json
product_id = data.get('product_id')
quantity = data.get('quantity', 1)
price = data.get('price', 0)
# Track business event
telemetry.track_event('Purchase', {
'product_id': product_id,
'quantity': quantity,
'total_amount': price * quantity,
'user_id': request.headers.get('X-User-Id', 'anonymous')
})
# Track revenue metric
telemetry.track_metric('Revenue', price * quantity, {
'product_id': product_id,
'currency': 'USD'
})
return jsonify({
'order_id': f'ORD-{random.randint(1000, 9999)}',
'status': 'confirmed',
'total': price * quantity
})
@app.route('/api/search')
def search():
"""Track search queries"""
query = request.args.get('q', '')
start_time = time.time()
# Simulate search (would be real database query)
results = [{'id': 1, 'name': f'Result for {query}'}]
duration = (time.time() - start_time) * 1000 # Convert to ms
# Track search event
telemetry.track_event('Search', {
'query': query,
'results_count': len(results),
'duration_ms': duration
})
# Track search performance metric
telemetry.track_metric('SearchDuration', duration, {
'query_length': len(query)
})
return jsonify({'results': results, 'count': len(results)})
@app.route('/api/external-call')
def external_call():
"""Track external API dependency"""
import requests
start_time = time.time()
success = True
try:
# Simulate external API call
response = requests.get('https://api.example.com/data', timeout=5)
result = response.json()
except Exception as e:
success = False
result = {'error': str(e)}
duration = (time.time() - start_time) * 1000
# Track dependency
telemetry.track_dependency(
name='ExternalAPI',
dependency_type='HTTP',
duration=duration,
success=success
)
return jsonify(result)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)# Track purchase event
curl -X POST $APP_URL/api/purchase \
-H "Content-Type: application/json" \
-H "X-User-Id: user123" \
-d '{"product_id": 1, "quantity": 2, "price": 29.99}'
# Track search event
curl "$APP_URL/api/search?q=laptop"
# Track external dependency
curl $APP_URL/api/external-callView in Azure Portal:
Navigate to Application Insights → Logs, then run:
// View purchase events
traces
| where customDimensions.event_name == "Purchase"
| project
timestamp,
product_id = tostring(customDimensions.product_id),
total_amount = todouble(customDimensions.total_amount),
user_id = tostring(customDimensions.user_id)
| order by timestamp desc
// View revenue metrics
traces
| where customDimensions.metric_name == "Revenue"
| summarize TotalRevenue = sum(todouble(customDimensions.value)) by bin(timestamp, 1h)
| render timechart
// View search performance
traces
| where customDimensions.event_name == "Search"
| summarize
AvgDuration = avg(todouble(customDimensions.duration_ms)),
SearchCount = count()
by bin(timestamp, 5m)
| render timechartFor microservices, Application Insights automatically correlates requests across services.
File: infra/main.bicep
targetScope = 'subscription'
param environmentName string
param location string = 'eastus'
var tags = { 'azd-env-name': environmentName }
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'rg-${environmentName}'
location: location
tags: tags
}
// Monitoring (shared by all services)
module monitoring './core/monitoring.bicep' = {
name: 'monitoring'
scope: rg
params: {
logAnalyticsName: 'log-${environmentName}'
applicationInsightsName: 'appi-${environmentName}'
location: location
tags: tags
}
}
// API Gateway
module apiGateway './app/api-gateway.bicep' = {
name: 'api-gateway'
scope: rg
params: {
name: 'ca-gateway-${environmentName}'
location: location
tags: union(tags, { 'azd-service-name': 'gateway' })
applicationInsightsConnectionString: monitoring.outputs.applicationInsightsConnectionString
}
}
// Product Service
module productService './app/product-service.bicep' = {
name: 'product-service'
scope: rg
params: {
name: 'ca-products-${environmentName}'
location: location
tags: union(tags, { 'azd-service-name': 'products' })
applicationInsightsConnectionString: monitoring.outputs.applicationInsightsConnectionString
}
}
// Order Service
module orderService './app/order-service.bicep' = {
name: 'order-service'
scope: rg
params: {
name: 'ca-orders-${environmentName}'
location: location
tags: union(tags, { 'azd-service-name': 'orders' })
applicationInsightsConnectionString: monitoring.outputs.applicationInsightsConnectionString
}
}
output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString
output GATEWAY_URL string = apiGateway.outputs.urisequenceDiagram
participant User
participant Gateway as API Gateway<br/>(Trace ID: abc123)
participant Product as Product Service<br/>(Parent ID: abc123)
participant Order as Order Service<br/>(Parent ID: abc123)
participant AppInsights as Application Insights
User->>Gateway: POST /api/checkout
Note over Gateway: Start Trace: abc123
Gateway->>AppInsights: Log request (Trace ID: abc123)
Gateway->>Product: GET /products/123
Note over Product: Parent ID: abc123
Product->>AppInsights: Log dependency call
Product-->>Gateway: Product details
Gateway->>Order: POST /orders
Note over Order: Parent ID: abc123
Order->>AppInsights: Log dependency call
Order-->>Gateway: Order created
Gateway-->>User: Checkout complete
Gateway->>AppInsights: Log response (Duration: 450ms)
Note over AppInsights: Correlation by Trace ID
Query end-to-end trace:
// Find complete request flow
let traceId = "abc123..."; // Get from response header
dependencies
| union requests
| where operation_Id == traceId
| project
timestamp,
type = itemType,
name,
duration,
success,
cloud_RoleName
| order by timestamp ascLive Metrics provides real-time telemetry with <1 second latency.
Access Live Metrics:
# Get Application Insights resource
APPI_NAME=$(azd env get-values | grep APPLICATIONINSIGHTS_NAME | cut -d '=' -f2 | tr -d '"')
# Get resource group
RG_NAME=$(azd env get-values | grep AZURE_RESOURCE_GROUP | cut -d '=' -f2 | tr -d '"')
echo "Navigate to: Azure Portal → Resource Groups → $RG_NAME → $APPI_NAME → Live Metrics"What you see in real-time:
- ✅ Incoming request rate (requests/sec)
- ✅ Outgoing dependency calls
- ✅ Exception count
- ✅ CPU and memory usage
- ✅ Active server count
- ✅ Sample telemetry
# Generate load to see live metrics
for i in {1..100}; do
curl $APP_URL/api/products &
curl $APP_URL/api/search?q=test$i &
done
# Watch live metrics in Azure Portal
# You should see request rate spikeGoal: Create alerts for high error rates and slow responses.
Steps:
- Create alert for error rate:
# Get Application Insights resource ID
APPI_ID=$(az monitor app-insights component show \
--app $APPI_NAME \
--resource-group $RG_NAME \
--query "id" -o tsv)
# Create metric alert for failed requests
az monitor metrics alert create \
--name "High-Error-Rate" \
--resource-group $RG_NAME \
--scopes $APPI_ID \
--condition "count requests/failed > 10" \
--window-size 5m \
--evaluation-frequency 1m \
--description "Alert when error rate exceeds 10 per 5 minutes"- Create alert for slow responses:
az monitor metrics alert create \
--name "Slow-Responses" \
--resource-group $RG_NAME \
--scopes $APPI_ID \
--condition "avg requests/duration > 3000" \
--window-size 5m \
--evaluation-frequency 1m \
--description "Alert when average response time exceeds 3 seconds"- Create alert via Bicep (preferred for AZD):
File: infra/core/alerts.bicep
param applicationInsightsId string
param actionGroupId string = ''
param location string = resourceGroup().location
// High error rate alert
resource errorRateAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: 'high-error-rate'
location: 'global'
properties: {
description: 'Alert when error rate exceeds threshold'
severity: 2
enabled: true
scopes: [
applicationInsightsId
]
evaluationFrequency: 'PT1M'
windowSize: 'PT5M'
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: [
{
name: 'Error rate'
metricName: 'requests/failed'
operator: 'GreaterThan'
threshold: 10
timeAggregation: 'Count'
}
]
}
actions: actionGroupId != '' ? [
{
actionGroupId: actionGroupId
}
] : []
}
}
// Slow response alert
resource slowResponseAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: 'slow-responses'
location: 'global'
properties: {
description: 'Alert when response time is too high'
severity: 3
enabled: true
scopes: [
applicationInsightsId
]
evaluationFrequency: 'PT1M'
windowSize: 'PT5M'
criteria: {
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
allOf: [
{
name: 'Response duration'
metricName: 'requests/duration'
operator: 'GreaterThan'
threshold: 3000
timeAggregation: 'Average'
}
]
}
}
}
output errorAlertId string = errorRateAlert.id
output slowResponseAlertId string = slowResponseAlert.id- Test alerts:
# Generate errors
for i in {1..20}; do
curl $APP_URL/api/error-test
done
# Generate slow responses
for i in {1..10}; do
curl $APP_URL/api/slow
done
# Check alert status (wait 5-10 minutes)
az monitor metrics alert list \
--resource-group $RG_NAME \
--query "[].{Name:name, Enabled:enabled, State:properties.enabled}" \
--output table✅ Success Criteria:
- ✅ Alerts created successfully
- ✅ Alerts fire when thresholds exceeded
- ✅ Can view alert history in Azure Portal
- ✅ Integrated with AZD deployment
Time: 20-25 minutes
Goal: Build a dashboard showing key application metrics.
Steps:
- Create dashboard via Azure Portal:
Navigate to: Azure Portal → Dashboards → New Dashboard
- Add tiles for key metrics:
- Request count (last 24 hours)
- Average response time
- Error rate
- Top 5 slowest operations
- Geographic distribution of users
- Create dashboard via Bicep:
File: infra/core/dashboard.bicep
param dashboardName string
param applicationInsightsId string
param location string = resourceGroup().location
resource dashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = {
name: dashboardName
location: location
properties: {
lenses: [
{
order: 0
parts: [
// Request count
{
position: { x: 0, y: 0, rowSpan: 4, colSpan: 6 }
metadata: {
type: 'Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart'
inputs: [
{
name: 'resourceId'
value: applicationInsightsId
}
{
name: 'query'
value: '''
requests
| summarize RequestCount = count() by bin(timestamp, 1h)
| render timechart
'''
}
]
}
}
// Error rate
{
position: { x: 6, y: 0, rowSpan: 4, colSpan: 6 }
metadata: {
type: 'Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart'
inputs: [
{
name: 'resourceId'
value: applicationInsightsId
}
{
name: 'query'
value: '''
requests
| summarize
Total = count(),
Failed = countif(success == false)
| extend ErrorRate = (Failed * 100.0) / Total
| project ErrorRate
'''
}
]
}
}
]
}
]
}
}
output dashboardId string = dashboard.id- Deploy dashboard:
# Add to main.bicep
module dashboard './core/dashboard.bicep' = {
name: 'dashboard'
scope: rg
params: {
dashboardName: 'dashboard-${environmentName}'
applicationInsightsId: monitoring.outputs.applicationInsightsId
location: location
}
}
# Deploy
azd up✅ Success Criteria:
- ✅ Dashboard displays key metrics
- ✅ Can pin to Azure Portal home
- ✅ Updates in real-time
- ✅ Deployable via AZD
Time: 25-30 minutes
Goal: Track Microsoft Foundry Models usage (tokens, costs, latency).
Steps:
- Create AI monitoring wrapper:
File: src/ai_telemetry.py
from telemetry import telemetry
from openai import AzureOpenAI
import time
class MonitoredAzureOpenAI:
"""Microsoft Foundry Models client with automatic telemetry"""
def __init__(self, api_key, endpoint, api_version="2024-02-01"):
self.client = AzureOpenAI(
api_key=api_key,
api_version=api_version,
azure_endpoint=endpoint
)
def chat_completion(self, model: str, messages: list, **kwargs):
"""Track chat completion with telemetry"""
start_time = time.time()
try:
# Call Microsoft Foundry Models
response = self.client.chat.completions.create(
model=model,
messages=messages,
**kwargs
)
duration = (time.time() - start_time) * 1000 # ms
# Extract usage
usage = response.usage
prompt_tokens = usage.prompt_tokens
completion_tokens = usage.completion_tokens
total_tokens = usage.total_tokens
# Calculate cost (gpt-4.1 pricing)
prompt_cost = (prompt_tokens / 1000) * 0.03 # $0.03 per 1K tokens
completion_cost = (completion_tokens / 1000) * 0.06 # $0.06 per 1K tokens
total_cost = prompt_cost + completion_cost
# Track custom event
telemetry.track_event('OpenAI_Request', {
'model': model,
'prompt_tokens': prompt_tokens,
'completion_tokens': completion_tokens,
'total_tokens': total_tokens,
'duration_ms': duration,
'cost_usd': total_cost,
'success': True
})
# Track metrics
telemetry.track_metric('OpenAI_Tokens', total_tokens, {
'model': model,
'type': 'total'
})
telemetry.track_metric('OpenAI_Cost', total_cost, {
'model': model,
'currency': 'USD'
})
telemetry.track_metric('OpenAI_Duration', duration, {
'model': model
})
return response
except Exception as e:
duration = (time.time() - start_time) * 1000
telemetry.track_event('OpenAI_Request', {
'model': model,
'duration_ms': duration,
'success': False,
'error': str(e)
})
raise- Use monitored client:
from flask import Flask, request, jsonify
from ai_telemetry import MonitoredAzureOpenAI
import os
app = Flask(__name__)
# Initialize monitored OpenAI client
openai_client = MonitoredAzureOpenAI(
api_key=os.environ['AZURE_OPENAI_API_KEY'],
endpoint=os.environ['AZURE_OPENAI_ENDPOINT']
)
@app.route('/api/chat', methods=['POST'])
def chat():
data = request.json
user_message = data.get('message')
# Call with automatic monitoring
response = openai_client.chat_completion(
model='gpt-4.1',
messages=[
{'role': 'user', 'content': user_message}
]
)
return jsonify({
'response': response.choices[0].message.content,
'tokens': response.usage.total_tokens
})- Query AI metrics:
// Total AI spend over time
traces
| where customDimensions.event_name == "OpenAI_Request"
| where customDimensions.success == "True"
| summarize TotalCost = sum(todouble(customDimensions.cost_usd)) by bin(timestamp, 1h)
| render timechart
// Token usage by model
traces
| where customDimensions.event_name == "OpenAI_Request"
| summarize
TotalTokens = sum(toint(customDimensions.total_tokens)),
RequestCount = count()
by Model = tostring(customDimensions.model)
// Average latency
traces
| where customDimensions.event_name == "OpenAI_Request"
| summarize AvgDuration = avg(todouble(customDimensions.duration_ms))
| project AvgDurationSeconds = AvgDuration / 1000
// Cost per request
traces
| where customDimensions.event_name == "OpenAI_Request"
| extend Cost = todouble(customDimensions.cost_usd)
| summarize
TotalCost = sum(Cost),
RequestCount = count(),
AvgCostPerRequest = avg(Cost)✅ Success Criteria:
- ✅ Every OpenAI call tracked automatically
- ✅ Token usage and costs visible
- ✅ Latency monitored
- ✅ Can set budget alerts
Time: 35-45 minutes
Control costs by sampling telemetry:
from opencensus.trace.samplers import ProbabilitySampler
# Development: 100% sampling
sampler = ProbabilitySampler(rate=1.0)
# Production: 10% sampling (reduce costs by 90%)
sampler = ProbabilitySampler(rate=0.1)
# Adaptive sampling (automatically adjusts)
from opencensus.trace.samplers import AdaptiveSampler
sampler = AdaptiveSampler()In Bicep:
resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: applicationInsightsName
properties: {
SamplingPercentage: 10 // 10% sampling
}
}resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: logAnalyticsName
properties: {
retentionInDays: 30 // Minimum (cheapest)
// Options: 30, 31, 60, 90, 120, 180, 270, 365, 550, 730
}
}| Data Volume | Retention | Monthly Cost |
|---|---|---|
| 1 GB/month | 30 days | ~$2-5 |
| 5 GB/month | 30 days | ~$10-15 |
| 10 GB/month | 90 days | ~$25-40 |
| 50 GB/month | 90 days | ~$100-150 |
Free tier: 5 GB/month included
Test your understanding:
-
Q1: How does AZD provision Application Insights?
- A: Automatically via Bicep templates in
infra/core/monitoring.bicep
- A: Automatically via Bicep templates in
-
Q2: What environment variable enables Application Insights?
- A:
APPLICATIONINSIGHTS_CONNECTION_STRING
- A:
-
Q3: What are the three main telemetry types?
- A: Requests (HTTP calls), Dependencies (external calls), Exceptions (errors)
Hands-On Verification:
# Check if Application Insights is configured
azd env get-values | grep APPLICATIONINSIGHTS
# Verify telemetry is flowing
az monitor app-insights metrics show \
--app $APPI_NAME \
--resource-group $RG_NAME \
--metric "requests/count"Test your understanding:
-
Q1: How do you track custom business events?
- A: Use logger with
custom_dimensionsorTelemetryClient.track_event()
- A: Use logger with
-
Q2: What's the difference between events and metrics?
- A: Events are discrete occurrences, metrics are numerical measurements
-
Q3: How do you correlate telemetry across services?
- A: Application Insights automatically uses
operation_Idfor correlation
- A: Application Insights automatically uses
Hands-On Verification:
// Verify custom events
traces
| where customDimensions.event_name != ""
| summarize count() by tostring(customDimensions.event_name)Test your understanding:
-
Q1: What is sampling and why use it?
- A: Sampling reduces data volume (and cost) by only capturing a percentage of telemetry
-
Q2: How do you set up alerts?
- A: Use metric alerts in Bicep or Azure Portal based on Application Insights metrics
-
Q3: What's the difference between Log Analytics and Application Insights?
- A: Application Insights stores data in Log Analytics workspace; App Insights provides application-specific views
Hands-On Verification:
# Check sampling configuration
az monitor app-insights component show \
--app $APPI_NAME \
--resource-group $RG_NAME \
--query "properties.SamplingPercentage"-
Use correlation IDs
logger.info('Processing order', extra={ 'custom_dimensions': { 'order_id': order_id, 'user_id': user_id } })
-
Set up alerts for critical metrics
// Error rate, slow responses, availability -
Use structured logging
# ✅ GOOD: Structured logger.info('User signup', extra={'custom_dimensions': {'user_id': 123}}) # ❌ BAD: Unstructured logger.info(f'User 123 signed up')
-
Monitor dependencies
# Automatically track database calls, HTTP requests, etc. -
Use Live Metrics during deployments
-
Don't log sensitive data
# ❌ BAD logger.info(f'Login: {username}:{password}') # ✅ GOOD logger.info('Login attempt', extra={'custom_dimensions': {'username': username}})
-
Don't use 100% sampling in production
# ❌ Expensive sampler = ProbabilitySampler(rate=1.0) # ✅ Cost-effective sampler = ProbabilitySampler(rate=0.1)
-
Don't ignore dead letter queues
-
Don't forget to set data retention limits
Diagnosis:
# Check connection string is set
azd env get-values | grep APPLICATIONINSIGHTS
# Check application logs via Azure Monitor
azd monitor --logs
# Or use Azure CLI for Container Apps:
az containerapp logs show --name $APP_NAME --resource-group $RG_NAME --tail 50Solution:
# Verify connection string in Container App
az containerapp show \
--name $APP_NAME \
--resource-group $RG_NAME \
--query "properties.template.containers[0].env" \
| grep -i applicationinsightsDiagnosis:
# Check data ingestion
az monitor app-insights metrics show \
--app $APPI_NAME \
--resource-group $RG_NAME \
--metric "availabilityResults/count"Solution:
- Reduce sampling rate
- Decrease retention period
- Remove verbose logging
- ← Previous: Preflight Checks
- → Next: Deployment Guide
- 🏠 Course Home
- Microsoft Foundry Models Example - AI telemetry
- Microservices Example - Distributed tracing
You've learned:
- ✅ Automatic Application Insights provisioning with AZD
- ✅ Custom telemetry (events, metrics, dependencies)
- ✅ Distributed tracing across microservices
- ✅ Live metrics and real-time monitoring
- ✅ Alerts and dashboards
- ✅ AI/LLM application monitoring
- ✅ Cost optimization strategies
Key Takeaways:
- AZD provisions monitoring automatically - No manual setup
- Use structured logging - Makes querying easier
- Track business events - Not just technical metrics
- Monitor AI costs - Track tokens and spending
- Set up alerts - Be proactive, not reactive
- Optimize costs - Use sampling and retention limits
Next Steps:
- Complete the practical exercises
- Add Application Insights to your AZD projects
- Create custom dashboards for your team
- Learn Deployment Guide