Chapter 9

Middleware and observability

Add cross-cutting behavior and OpenTelemetry traces without burying operational concerns inside agent logic.

Objective

After this chapter, you will be able to add cross-cutting behavior with Microsoft Agent Framework middleware and turn on OpenTelemetry tracing without burying operational concerns inside agent instructions or tool code.

The examples target the verified Python package agent-framework==1.9.0 and run without live model keys by short-circuiting network calls or disabling exporters.

Concept/Theory

Middleware is a request/response pipeline around work the framework already knows how to do: running an agent, calling a chat client, or invoking a function tool. A middleware component can inspect the context before the call, continue with await call_next(), inspect or transform the result afterward, or deliberately stop the pipeline with a local result.

Observability is the companion operational overlay: traces, metrics, and logs that help you answer what happened, where time was spent, which path a workflow took, and why a run failed. Microsoft Learn describes Agent Framework observability as built-in monitoring support for understanding agent behavior and diagnosing issues.Microsoft Learn

This is the same idea Chapter 2 called operational overlays. Middleware and telemetry do not choose your agentic pattern. They make a single agent, tool loop, or workflow safer to operate by keeping policy, logging, redaction, retries, and diagnostics outside the business logic.

In MAF

Agent Framework Python 1.9.0 exposes three middleware layers plus OpenTelemetry setup. Microsoft Learn documents middleware as a way to intercept and customize agent behavior, and the verified package API supplies both class-based middleware and decorator-based middleware functions.Microsoft Learn

Middleware and observability APIs verified for agent-framework==1.9.0
Concern API symbols Register or call it Use for
Whole agent run AgentMiddleware, @agent_middleware Agent(..., middleware=[...]) or agent.run(..., middleware=[...]) Run-level logging, guardrails, retries, redaction, response shaping, and policy.
Chat/model request ChatMiddleware, @chat_middleware OpenAIChatClient(..., middleware=[...]) or get_response(..., middleware=[...]); it can also flow through an Agent middleware list. Provider request inspection, model-call caching, prompt/request policy, telemetry correlation, and no-network fakes.
Function/tool invocation FunctionMiddleware, @function_middleware Agent(..., middleware=[...]), agent.run(..., middleware=[...]), OpenAIChatClient(..., middleware=[...]), or get_response(..., middleware=[...]). Tool authorization, argument allowlists, idempotency, caching, audits, and tool-result masking.
OpenTelemetry providers agent_framework.observability.configure_otel_providers Call once at application startup before agent or workflow calls. Configure traces, metrics, logs, console exporters, OTLP exporters, VS Code extension output, and sensitive-data behavior.

Middleware receives a mutable context and a call_next callback. If the middleware sets context.result and raises MiddlewareTermination, agent and chat pipelines can return without making the remaining call. Use this deliberately for fakes, cache hits, or policy blocks; use ordinary exceptions for real failures.

backend/examples/ch09/middleware_demo.py: an agent middleware logs the run, while chat middleware returns a local response before network I/O.
from agent_framework import Agent, ChatResponse, Content, Message, MiddlewareTermination
from agent_framework import agent_middleware, chat_middleware
from agent_framework.openai import OpenAIChatClient

@agent_middleware
async def log_agent_run(context, call_next):
    print(f"agent middleware saw {len(context.messages)} message(s)")
    context.metadata["chapter"] = "09"
    await call_next()
    print(f"agent middleware result: {context.result.text}")

@chat_middleware
async def local_chat_response(context, call_next):
    context.result = ChatResponse(
        messages=Message(role="assistant", contents=[Content.from_text("local chat response")]),
        model="middleware-local",
    )
    raise MiddlewareTermination(result=context.result)

client = OpenAIChatClient(model="not-called", api_key="placeholder")
agent = Agent(client=client, name="middleware-demo", middleware=[log_agent_run, local_chat_response])

For observability, use the installed setup function configure_otel_providers. The no-network example below disables console exporters and sensitive telemetry. Supplying real OTLP exporters, Azure Monitor, Langfuse, or other exporter destinations is a credential/exporter-requiring production step.

backend/examples/ch09/observability_setup.py: configure local OpenTelemetry providers, create a tracer and meter, and keep sensitive telemetry disabled.
from agent_framework.observability import (
    OBSERVABILITY_SETTINGS,
    configure_otel_providers,
    enable_instrumentation,
    get_meter,
    get_tracer,
)

configure_otel_providers(enable_console_exporters=False, enable_sensitive_data=False)
enable_instrumentation(enable_sensitive_data=False, force=True)

tracer = get_tracer("playbook.ch09")
meter = get_meter("playbook.ch09")
counter = meter.create_counter("playbook_ch09_observability_runs")
counter.add(1, {"example": "observability_setup"})

with tracer.start_as_current_span("playbook.ch09.no_network_probe") as span:
    span.set_attribute("example.chapter", "09")

print(f"sensitive telemetry enabled: {OBSERVABILITY_SETTINGS.SENSITIVE_DATA_ENABLED}")

When to use / pitfalls

Choosing middleware, tools, instructions, and telemetry
Use When it fits Avoid when
Instructions You need role, tone, response format, or broad behavioral guidance. You need enforceable validation, auditing, retries, or data redaction.
Tools The agent needs a narrow capability such as lookup, calculation, or a controlled business action. The concern applies to every run or every tool call regardless of which tool is selected.
Middleware You need cross-cutting behavior around agent runs, chat requests, or function invocations. The logic is the task itself; then make it an ordinary function, tool, workflow step, or application service.
Observability You need to debug failures, monitor latency/cost, correlate model calls and tools, or inspect workflow paths. The run is a tiny local script and traces would add more noise than value.

Common pitfalls

  • Putting policy only in prompts: instructions can guide behavior, but middleware can enforce application-side checks before or after framework calls.
  • Choosing the wrong layer: use agent middleware for whole-run concerns, chat middleware for provider request/response concerns, and function middleware for tool-call validation or audit.
  • Hiding business behavior in middleware: middleware should be reusable cross-cutting logic, not the primary domain workflow.
  • Leaking telemetry data: prompts, messages, arguments, and outputs can contain sensitive data. In the verified 1.9.0 package, sensitive telemetry is off by default; keep enable_sensitive_data=False in production unless you have explicit approval and controls.
  • Forgetting exporter requirements: local provider setup can run without credentials, but real OTLP or cloud exporters need configured endpoints, credentials, and operational ownership.
  • Configuring OpenTelemetry repeatedly: call configure_otel_providers once at startup, before agent or workflow execution.

Worked example

The chapter examples are intentionally runnable without live keys. middleware_demo.py constructs an OpenAIChatClient with a placeholder key, but the chat middleware raises MiddlewareTermination with a local ChatResponse, so the provider is never called. observability_setup.py configures local OpenTelemetry providers with exporters disabled and sensitive telemetry disabled.

  1. Run python backend\examples\ch09\middleware_demo.py to see the agent middleware wrap a chat middleware short-circuit.
  2. Run python backend\examples\ch09\observability_setup.py to verify tracing and metrics setup without exporting telemetry.
  3. For production observability, add a real exporter or environment variables such as OTEL_EXPORTER_OTLP_*; that step requires an endpoint and credentials.

Expected behavior: the middleware demo prints that the agent middleware saw one message, the chat middleware short-circuited the request, and the final response is local chat response. The observability demo prints that instrumentation is enabled and sensitive telemetry is disabled.

Recap & next

  • Middleware is the request/response pipeline for cross-cutting behavior around agent runs, chat/model calls, and function/tool invocations.
  • Use AgentMiddleware, ChatMiddleware, and FunctionMiddleware or their decorators when the concern should stay outside agent instructions and domain tools.
  • Use configure_otel_providers once at startup to wire OpenTelemetry providers; keep sensitive telemetry disabled unless explicitly approved.
  • The verified examples for this chapter run without live credentials; real exporters and cloud destinations require configuration and credentials.

Next, Chapter 10 moves from local code to declarative agents, hosting, and DevUI so the same agent behaviors can be configured, run, and debugged in richer environments.