Exercise - Instrument an app with the OpenTelemetry SDK
Hands-on lab: create Application Insights, instrument a Flask app with the Azure Monitor distro, add custom spans and verify them with KQL.
What this covers
This exercise instruments a small Python AI-style API end to end: create a Log Analytics workspace and a workspace-based Application Insights resource, configure the Azure Monitor OpenTelemetry Distro, add a custom span with attributes for an "embedding" step, and confirm the telemetry in the portal and with KQL. The official Learn exercise uses a Flask document-processing pipeline with parent and child spans and has you diagnose a simulated latency bottleneck; the steps below follow the same shape.
Exam weight: Secure, monitor, and troubleshoot Azure solutions (20–25%)
Skills measured
- "Trace distributed systems by using OpenTelemetry SDKs"
- "Write KQL queries to analyze logs and metrics"
Key concepts
Goals
- Create (or reuse) a Log Analytics workspace + Application Insights resource
- Install and configure the Azure Monitor OpenTelemetry distro
- Add a custom span with attributes for an "embedding" step
- Confirm telemetry in
AppRequests/AppDependenciesand the custom attributes
Prerequisites
- Azure subscription, Azure CLI logged in (
az login) - Python 3.10+ (the Learn lab asks for 3.12+) and VS Code
- A resource group, for example
<resource-group>
What you should see
| Telemetry | Where it lands | Why |
|---|---|---|
GET /embed | AppRequests (requests) | Flask instrumentation creates a SERVER span |
generate_embedding | AppDependencies (dependencies), type InProc | Custom span defaults to SpanKind.INTERNAL |
gen_ai.model, documents.count | Properties (customDimensions) on the dependency row | Span attributes become custom dimensions |
| Shared ID | OperationId (operation_Id) | Both spans belong to one trace |
How it works
Hands-on
Azure CLI
Step 1: create monitoring resources and read the connection string.
az monitor log-analytics workspace create \
--resource-group <resource-group> \
--workspace-name <workspace-name> \
--location eastus
az monitor app-insights component create \
--app <app-insights-name> \
--location eastus \
--resource-group <resource-group> \
--workspace <workspace-name>
az monitor app-insights component show \
--app <app-insights-name> \
--resource-group <resource-group> \
--query connectionString -o tsvStep 2: install packages.
pip install azure-monitor-opentelemetry flaskPython
Step 3: instrument a minimal app (app.py). Set the service name before calling configure_azure_monitor().
import os
from flask import Flask, jsonify
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace
os.environ.setdefault("OTEL_SERVICE_NAME", "ai200-demo-api")
configure_azure_monitor(
connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"],
)
app = Flask(__name__)
tracer = trace.get_tracer(__name__)
@app.get("/embed")
def embed():
with tracer.start_as_current_span("generate_embedding") as span:
span.set_attribute("gen_ai.model", "text-embedding-3-large")
span.set_attribute("documents.count", 1)
# pretend work
vector = [0.1, 0.2, 0.3]
return jsonify({"dims": len(vector)})
if __name__ == "__main__":
app.run(port=8080)Step 4: generate traffic, then wait 1–2 minutes for ingestion.
export APPLICATIONINSIGHTS_CONNECTION_STRING="..."
python app.py
# in another shell
curl http://127.0.0.1:8080/embedTo mimic the lab's latency bottleneck, add time.sleep(2) inside the span and compare durations.
Azure portal
Step 5: verify.
- Open the Application Insights resource → Live Metrics while you send requests; you should see them within seconds.
- Open Transaction search, find
GET /embed, open it and confirmgenerate_embeddingis nested under it with the two custom properties. - Open Logs and run the queries below (run each one separately).
Workspace names:
AppRequests
| where TimeGenerated > ago(15m)
| where Name has "embed"
| project TimeGenerated, Name, Success, DurationMs, OperationId, AppRoleNameAppDependencies
| where TimeGenerated > ago(15m)
| where Name == "generate_embedding"
| project TimeGenerated, Name, DurationMs, OperationId,
model = tostring(Properties["gen_ai.model"])
| take 20App Insights scope (classic names):
dependencies
| where timestamp > ago(15m)
| where name == "generate_embedding"
| project timestamp, duration, operation_Id, customDimensionsReconstruct one operation:
union AppRequests, AppDependencies
| where OperationId == "<operation-id>"
| project TimeGenerated, Type, Name, DurationMs
| order by TimeGenerated ascChecklist
- Connection string set;
AppRoleNameisai200-demo-api, notunknown_service - Request appears in
AppRequests - Custom attributes visible under
Properties(customDimensionsin App Insights scope) - You can copy
OperationIdand reconstruct the operation withunion
Exam traps
| Symptom during the lab | Fix |
|---|---|
| No data after 5 minutes | Check the connection string and outbound HTTPS; call configure_azure_monitor() before creating the Flask app |
Role shows unknown_service | Set OTEL_SERVICE_NAME before configuring the distro |
Custom span missing from AppRequests | Expected: INTERNAL spans are in AppDependencies |
customDimensions not found in AppDependencies | Use Properties in workspace tables |
| Some requests missing under load | Default rate-limited sampling (~5 traces/sec) |
Quick review
- One workspace + one workspace-based App Insights resource is all the infrastructure you need.
pip install azure-monitor-opentelemetry, then callconfigure_azure_monitor()once.- Flask requests appear automatically in
AppRequests. - Custom spans default to INTERNAL and land in
AppDependencies. - Attributes appear in
Properties/customDimensions. - Expect 1–3 minutes of ingestion delay; Live Metrics is near real time.
Practice
Practice: OpenTelemetry questions · Practice: KQL questions