Python: improve .env handling and observability samples (#4032)

* Python: improve .env precedence and observability samples

- Switch load_settings to explicit precedence: overrides -> explicit .env -> environment -> defaults\n- Raise when env_file_path is provided but missing\n- Update settings docs and tests for new behavior\n- Refresh observability samples and README guidance for env loading options\n\nCloses #3864\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fixed some imports

* Fix load_settings CI regressions

Allow explicit env_file_path values that exist but are not regular files (for example /dev/null) by checking path existence before dotenv parsing, and restore a dict accumulator with typed return cast to satisfy mypy.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Avoid implicit dotenv in observability

Only load dotenv in observability helpers when env_file_path is explicitly provided, and remove test os.devnull workarounds that are no longer necessary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-02-18 12:18:52 +01:00
committed by GitHub
Unverified
parent f900febb6f
commit 534e5f5bf7
41 changed files with 250 additions and 221 deletions
@@ -1,4 +1,4 @@
# Agent Framework Python Observability
# Agent Framework Observability
This sample folder shows how a Python application can be configured to send Agent Framework observability data to the Application Performance Management (APM) vendor(s) of your choice based on the OpenTelemetry standard.
@@ -222,7 +222,15 @@ This folder contains different samples demonstrating how to use telemetry in var
1. Open a terminal and navigate to this folder: `python/samples/02-agents/observability/`. This is necessary for the `.env` file to be read correctly.
2. Create a `.env` file if one doesn't already exist in this folder. Please refer to the [example file](./.env.example).
> **Note**: You can start with just `ENABLE_INSTRUMENTATION=true` and add `OTEL_EXPORTER_OTLP_ENDPOINT` or other configuration as needed. If no exporters are configured, you can set `ENABLE_CONSOLE_EXPORTERS=true` for console output.
3. Activate your python virtual environment, and then run `python configure_otel_providers_with_env_var.py` or others.
3. Choose one environment-loading approach:
- **A. Sample-managed loading (current samples):** run from this folder so the sample's `load_dotenv()` call can find `.env`.
- **B. Shell/IDE-managed environment:** set/export environment variables directly, or use an IDE run configuration that injects env vars / `.env`.
- **C. Explicit env file in code:** pass `env_file_path` to APIs like `configure_otel_providers(env_file_path=".env")` (or your own settings loader path).
- **D. CLI-managed env file:** run with `uv` and pass the file explicitly, for example:
`uv run --env-file=.env python configure_otel_providers_with_env_var.py`
4. Activate your python virtual environment, then run a sample (for example `python configure_otel_providers_with_env_var.py`).
> If you do manual provider setup (e.g., Azure Monitor), call `enable_instrumentation()` to turn on Agent Framework telemetry code paths; if you want Agent Framework to configure exporters/providers for you, call `configure_otel_providers(...)`.
> Each sample will print the Operation/Trace ID, which can be used later for filtering logs and traces in Application Insights or Aspire Dashboard.
@@ -5,7 +5,7 @@ import logging
from random import randint
from typing import Annotated
from agent_framework import tool
from agent_framework import Message, tool
from agent_framework.observability import enable_instrumentation
from agent_framework.openai import OpenAIChatClient
from opentelemetry._logs import set_logger_provider
@@ -66,7 +66,9 @@ def setup_metrics():
set_meter_provider(meter_provider)
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -107,9 +109,9 @@ async def run_chat_client() -> None:
message = "What's the weather in Amsterdam and in Paris?"
print(f"User: {message}")
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
async for chunk in client.get_response([Message(role="user", text=message)], tools=get_weather, stream=True):
if chunk.text:
print(chunk.text, end="")
print("")
@@ -4,7 +4,7 @@ import asyncio
from random import randint
from typing import TYPE_CHECKING, Annotated
from agent_framework import tool
from agent_framework import Message, tool
from agent_framework.observability import get_tracer
from agent_framework.openai import OpenAIResponsesClient
from opentelemetry.trace import SpanKind
@@ -20,13 +20,14 @@ This sample shows how you can configure observability of an application with zer
It relies on the OpenTelemetry auto-instrumentation capabilities, and the observability setup
is done via environment variables.
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool.
Follow the install guidance from https://opentelemetry.io/docs/zero-code/python/ to install the OpenTelemetry CLI tool,
when using `uv` there are some additional steps, so follow the instructions carefully.
And setup a local OpenTelemetry Collector instance to receive the traces and metrics (and update the endpoint below).
Then you can run:
```bash
opentelemetry-enable_instrumentation \
opentelemetry-instrument \
--traces_exporter otlp \
--metrics_exporter otlp \
--service_name agent_framework \
@@ -40,7 +41,9 @@ You can also set the environment variables instead of passing them as CLI argume
"""
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -81,12 +84,12 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
async for chunk in client.get_response([Message(role="user", text=message)], tools=get_weather, stream=True):
if chunk.text:
print(chunk.text, end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
print(f"Assistant: {response}")
@@ -53,11 +53,7 @@ async def main():
for question in questions:
print(f"\nUser: {question}")
print(f"{agent.name}: ", end="")
async for update in agent.run(
question,
session=session,
stream=True,
):
async for update in agent.run(question, session=session, stream=True):
if update.text:
print(update.text, end="")
@@ -15,13 +15,13 @@ import os
from random import randint
from typing import Annotated
import dotenv
from agent_framework import Agent, tool
from agent_framework.observability import create_resource, enable_instrumentation, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
from azure.monitor.opentelemetry import configure_azure_monitor
from dotenv import load_dotenv
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
from pydantic import Field
@@ -36,12 +36,14 @@ So ensure you have the `azure-monitor-opentelemetry` package installed.
"""
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
dotenv.load_dotenv()
load_dotenv()
logger = logging.getLogger(__name__)
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -5,12 +5,12 @@ import os
from random import randint
from typing import Annotated
import dotenv
from agent_framework import Agent, tool
from agent_framework.azure import AzureAIClient
from agent_framework.observability import get_tracer
from azure.ai.projects.aio import AIProjectClient
from azure.identity.aio import AzureCliCredential
from dotenv import load_dotenv
from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import format_trace_id
from pydantic import Field
@@ -26,10 +26,12 @@ for this sample to work.
"""
# For loading the `AZURE_AI_PROJECT_ENDPOINT` environment variable
dotenv.load_dotenv()
load_dotenv()
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -6,7 +6,7 @@ from contextlib import suppress
from random import randint
from typing import TYPE_CHECKING, Annotated, Literal
from agent_framework import tool
from agent_framework import Message, tool
from agent_framework.observability import configure_otel_providers, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from opentelemetry import trace
@@ -31,7 +31,9 @@ output traces, logs, and metrics to the console.
SCENARIOS = ["client", "client_stream", "tool", "all"]
# NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
# NOTE: approval_mode="never_require" is for sample brevity.
# Use "always_require" in production; see samples/02-agents/tools/function_tool_with_approval.py
# and samples/02-agents/tools/function_tool_with_approval_and_sessions.py.
@tool(approval_mode="never_require")
async def get_weather(
location: Annotated[str, Field(description="The location to get the weather for.")],
@@ -71,12 +73,14 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, tools=get_weather, stream=True):
if str(chunk):
print(str(chunk), end="")
async for chunk in client.get_response(
[Message(role="user", text=message)], tools=get_weather, stream=True
):
if chunk.text:
print(chunk.text, end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
print(f"Assistant: {response}")
@@ -92,8 +96,7 @@ async def run_tool() -> None:
"""
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
print("Running scenario: AI Function")
func = tool(get_weather)
weather = await func.invoke(location="Amsterdam")
weather = await get_weather.invoke(location="Amsterdam")
print(f"Weather in Amsterdam:\n{weather}")
@@ -7,7 +7,7 @@ from contextlib import suppress
from random import randint
from typing import TYPE_CHECKING, Annotated, Literal
from agent_framework import tool
from agent_framework import Message, tool
from agent_framework.observability import configure_otel_providers, get_tracer
from agent_framework.openai import OpenAIResponsesClient
from opentelemetry import trace
@@ -74,12 +74,14 @@ async def run_chat_client(client: "SupportsChatGetResponse", stream: bool = Fals
print(f"User: {message}")
if stream:
print("Assistant: ", end="")
async for chunk in client.get_response(message, stream=True, tools=get_weather):
if str(chunk):
print(str(chunk), end="")
async for chunk in client.get_response(
[Message(role="user", text=message)], stream=True, tools=get_weather
):
if chunk.text:
print(chunk.text, end="")
print("")
else:
response = await client.get_response(message, tools=get_weather)
response = await client.get_response([Message(role="user", text=message)], tools=get_weather)
print(f"Assistant: {response}")
@@ -95,8 +97,7 @@ async def run_tool() -> None:
"""
with get_tracer().start_as_current_span("Scenario: AI Function", kind=trace.SpanKind.CLIENT):
print("Running scenario: AI Function")
func = tool(get_weather)
weather = await func.invoke(location="Amsterdam")
weather = await get_weather.invoke(location="Amsterdam")
print(f"Weather in Amsterdam:\n{weather}")