Enable Tracing & Logging for MAF Workflows
Configure OpenTelemetry-based tracing for Microsoft Agent Framework workflows, exporting to Azure Application Insights and/or a generic OTLP endpoint.
Triggers
Activate this skill when the user wants to:
- Enable tracing or logging for a MAF workflow
- Export telemetry to Azure Application Insights
- Export traces to an OTLP-compatible endpoint (Jaeger, Zipkin, Grafana Tempo, etc.)
- Add observability or monitoring to an
agent-frameworkproject - Debug or inspect executor/agent/LLM spans in a MAF workflow
Background
MAF automatically emits OpenTelemetry spans for every executor invocation, agent call, and LLM request. No instrumentation changes are needed inside Executor classes. You only need to:
- Configure an exporter — where spans are sent (Application Insights, OTLP endpoint, or both)
- Call
configure_otel_providers()— activates MAF's built-in instrumentation
This must happen once at application startup, before any workflow.run() calls.
Rules
- Read the user's project first — Check for an existing
requirements.txt,.env, and entry-point script (e.g.,main.py,app.py,run_*.py). - Ask what export destination(s) are needed — Application Insights, OTLP endpoint, or both. If unclear, default to both.
- Do not modify Executor classes — MAF tracing is automatic. Never add manual
tracer.start_span()calls inside@handlermethods unless the user explicitly asks for custom spans. - Call order matters — Exporter configuration must happen BEFORE
configure_otel_providers(), and both must happen BEFORE anyworkflow.run(). - Add packages to
requirements.txt— Append only the packages the user needs (see Packages section). Do not duplicate existing entries. - Use environment variables — Never hardcode connection strings or endpoints. Always read from
os.environor.env. - Generate
.env.example— Provide a template showing which environment variables are required. - Python logging integration — When the user asks for logging (not just tracing), also configure the Python
loggingmodule to export via OpenTelemetry usingopentelemetry-sdklog handler.
Environment Variables
| Variable | Required For | Description |
|---|---|---|
APPLICATIONINSIGHTS_CONNECTION_STRING |
Application Insights | Connection string from Azure Portal → App Insights → Overview |
OTEL_EXPORTER_OTLP_ENDPOINT |
OTLP export | Base URL of the OTLP collector (e.g., http://localhost:4318) |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT |
OTLP export (traces only) | Overrides the base endpoint for trace signals only |
OTEL_EXPORTER_OTLP_PROTOCOL |
OTLP export | Protocol: http/protobuf (default) or grpc |
OTEL_SERVICE_NAME |
Optional | Service name shown in trace backends (defaults to Python process name) |
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTtakes precedence overOTEL_EXPORTER_OTLP_ENDPOINTfor traces.
Packages
| Package | Version | When Needed |
|---|---|---|
agent-framework |
>=1.0.1 | Always (provides configure_otel_providers) |
azure-monitor-opentelemetry |
>=1.6.4 | Application Insights export |
opentelemetry-exporter-otlp-proto-http |
>=1.25.0 | OTLP/HTTP export |
opentelemetry-exporter-otlp-proto-grpc |
>=1.25.0 | OTLP/gRPC export (only if OTEL_EXPORTER_OTLP_PROTOCOL=grpc) |
opentelemetry-sdk |
>=1.25.0 | Custom spans or Python logging integration |
python-dotenv |
any | Loading .env files |
Setup Patterns
Pattern A: Application Insights Only
Use when the user wants to send traces to Azure Application Insights.
Required env var: APPLICATIONINSIGHTS_CONNECTION_STRING
Required packages:
azure-monitor-opentelemetry>=1.6.4
Setup code (add at the top of the entry-point script, before workflow.run()):
import os
from dotenv import load_dotenv
from azure.monitor.opentelemetry import configure_azure_monitor
from agent_framework.observability import configure_otel_providers
load_dotenv()
# Step 1: Configure Azure Monitor exporter (traces, metrics, logs → App Insights)
configure_azure_monitor(
connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
)
# Step 2: Enable MAF's built-in instrumentation (executor, agent, LLM spans)
configure_otel_providers()
Pattern B: OTLP Endpoint Only
Use when the user wants to send traces to a generic OTLP-compatible backend (Jaeger, Grafana Tempo, Aspire Dashboard, etc.).
Required env var: OTEL_EXPORTER_OTLP_ENDPOINT
Required packages:
opentelemetry-sdk>=1.25.0
opentelemetry-exporter-otlp-proto-http>=1.25.0
Setup code:
import os
from dotenv import load_dotenv
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from agent_framework.observability import configure_otel_providers
load_dotenv()
# Step 1: Set up the OTLP exporter with a TracerProvider
resource = Resource.create({
"service.name": os.environ.get("OTEL_SERVICE_NAME", "maf-workflow"),
})
tracer_provider = TracerProvider(resource=resource)
otlp_exporter = OTLPSpanExporter(
endpoint=os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"),
)
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(tracer_provider)
# Step 2: Enable MAF's built-in instrumentation
configure_otel_providers()
Pattern C: Both Application Insights and OTLP
Use when the user wants dual export — Application Insights for Azure-native monitoring plus an OTLP backend for local/third-party observability.
Required env vars: APPLICATIONINSIGHTS_CONNECTION_STRING, OTEL_EXPORTER_OTLP_ENDPOINT
Required packages:
azure-monitor-opentelemetry>=1.6.4
opentelemetry-sdk>=1.25.0
opentelemetry-exporter-otlp-proto-http>=1.25.0
Setup code:
import os
from dotenv import load_dotenv
from azure.monitor.opentelemetry import configure_azure_monitor
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from agent_framework.observability import configure_otel_providers
load_dotenv()
# Step 1a: Configure Azure Monitor (sets up its own TracerProvider internally)
configure_azure_monitor(
connection_string=os.environ["APPLICATIONINSIGHTS_CONNECTION_STRING"]
)
# Step 1b: Add OTLP exporter to the existing TracerProvider
otlp_endpoint = (
os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
)
if otlp_endpoint:
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
tracer_provider: TracerProvider = trace.get_tracer_provider()
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
# Step 2: Enable MAF's built-in instrumentation
configure_otel_providers()
Pattern D: Python Logging via OpenTelemetry
Use when the user also wants Python logging calls to be exported alongside traces.
Additional packages (on top of Pattern A, B, or C):
opentelemetry-sdk>=1.25.0
Setup code (add after the exporter setup, before configure_otel_providers()):
import logging
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry._logs import set_logger_provider
# If using Application Insights, configure_azure_monitor() already handles log export.
# If using OTLP only, set up the OTLP log exporter:
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(
endpoint=os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT"),
))
)
set_logger_provider(logger_provider)
# Bridge Python logging to OpenTelemetry
from opentelemetry.instrumentation.logging import LoggingInstrumentor
LoggingInstrumentor().instrument(set_logging_format=True)
Note: When using
azure-monitor-opentelemetry(Pattern A/C),configure_azure_monitor()already captures Python logs by default. The above is only needed for OTLP-only setups.
Implementation Steps
Step 1: Determine export destination(s)
Ask the user or infer from context:
- Application Insights → Pattern A
- OTLP endpoint → Pattern B
- Both → Pattern C
If the user says "tracing" without specifying a destination, default to Pattern C (both).
Step 2: Update requirements.txt
Append the required packages. Do not duplicate existing entries. Example additions for Pattern C:
azure-monitor-opentelemetry>=1.6.4
opentelemetry-sdk>=1.25.0
opentelemetry-exporter-otlp-proto-http>=1.25.0
Step 3: Create or update .env.example
Add the environment variables relevant to the chosen pattern:
# === Tracing & Observability ===
# Application Insights (Azure Portal → App Insights → Overview → Connection String)
APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=xxx;IngestionEndpoint=https://xxx.in.applicationinsights.azure.com/
# OTLP endpoint (e.g., Jaeger, Grafana Tempo, Aspire Dashboard)
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
# Optional: override service name in trace backends
OTEL_SERVICE_NAME=my-maf-workflow
Step 4: Add tracing setup to entry-point script
Insert the setup code from the appropriate pattern at the top of the entry-point script (after imports, before any workflow.run() call). The setup must execute once at module load / application startup.
Placement rules:
- If the entry point uses
asyncio.run(main()), place setup code insidemain()beforeworkflow.run(). - If the entry point is a module-level script, place setup code after imports and
load_dotenv(). - If the entry point is a web server (FastAPI, Flask), place setup code in the application factory or startup event.
Step 5: Verify
- Run the workflow and check that traces appear in the configured destination.
- For Application Insights: Azure Portal → Application Insights → Transaction Search → filter by "Dependency" or "Request".
- For OTLP: Check the collector backend UI (e.g., Jaeger UI at
http://localhost:16686).
Gotchas
- Call order matters —
configure_azure_monitor()and/or OTLP exporter setup must happen BEFOREconfigure_otel_providers(). If reversed, MAF spans won't be exported. configure_otel_providers()must run BEFOREworkflow.run()— Otherwise, executor-level spans are not generated.- Do not call setup code inside Executors — Tracing setup is application-level, not per-request. Calling it inside a
@handlermethod will create duplicate exporters and corrupt traces. configure_azure_monitor()creates its own TracerProvider — When combining with OTLP (Pattern C), add the OTLP exporter to the existing provider viatrace.get_tracer_provider()rather than creating a newTracerProvider.- Missing
configure_otel_providers()— Without this call, you'll see Application Insights or OTLP infrastructure telemetry but no MAF-specific spans (executor transitions, agent calls, LLM requests). - Connection string format — The
APPLICATIONINSIGHTS_CONNECTION_STRINGstarts withInstrumentationKey=followed by a GUID. Do not confuse it with the Instrumentation Key alone. - OTLP endpoint trailing path —
OTEL_EXPORTER_OTLP_ENDPOINTshould be the base URL (e.g.,http://localhost:4318). The SDK appends/v1/tracesautomatically. Do not include/v1/tracesin the env var. - gRPC vs HTTP — Default protocol is
http/protobuf(port 4318). If the collector uses gRPC (port 4317), setOTEL_EXPORTER_OTLP_PROTOCOL=grpcand installopentelemetry-exporter-otlp-proto-grpcinstead.
Example: Complete Entry Point with Tracing
"""Entry point for a MAF workflow with full tracing setup."""
import asyncio
import os
from dotenv import load_dotenv
load_dotenv()
def setup_tracing():
"""Configure telemetry exporters and MAF instrumentation. Call once at startup."""
from agent_framework.observability import configure_otel_providers
# Application Insights
appinsights_conn = os.environ.get("APPLICATIONINSIGHTS_CONNECTION_STRING")
if appinsights_conn:
from azure.monitor.opentelemetry import configure_azure_monitor
configure_azure_monitor(connection_string=appinsights_conn)
# OTLP endpoint (optional, additive)
otlp_endpoint = (
os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
or os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
)
if otlp_endpoint:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
tracer_provider = trace.get_tracer_provider()
# If Azure Monitor already set a TracerProvider, reuse it; otherwise create one
if not isinstance(tracer_provider, TracerProvider):
resource = Resource.create({
"service.name": os.environ.get("OTEL_SERVICE_NAME", "maf-workflow"),
})
tracer_provider = TracerProvider(resource=resource)
trace.set_tracer_provider(tracer_provider)
tracer_provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
# Enable MAF's built-in spans (must be last)
configure_otel_providers()
async def main():
setup_tracing()
# Import and run your workflow here
from workflow import create_workflow
workflow = create_workflow()
result = await workflow.run("Hello, world!")
print(result.get_outputs())
if __name__ == "__main__":
asyncio.run(main())