Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions python/packages/core/agent_framework/observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,25 @@ def get_meter(
OBSERVABILITY_SETTINGS: ObservabilitySettings = ObservabilitySettings()


def _read_bool_env(name: str, *, default: bool = False) -> bool:
"""Read a boolean from an environment variable."""
value = os.getenv(name)
if value is None:
return default
return value.lower() in ("true", "1", "yes", "on")


def _read_int_env(name: str, *, default: int | None = None) -> int | None:
"""Read an optional integer from an environment variable."""
value = os.getenv(name)
if value is None:
return default
try:
return int(value)
except ValueError:
return default


def enable_instrumentation(
*,
enable_sensitive_data: bool | None = None,
Expand All @@ -920,11 +939,15 @@ def enable_instrumentation(
OBSERVABILITY_SETTINGS.enable_instrumentation = True
if enable_sensitive_data is not None:
OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data
else:
# Re-read from current environment in case env vars were set after import (e.g. load_dotenv())
OBSERVABILITY_SETTINGS.enable_sensitive_data = _read_bool_env("ENABLE_SENSITIVE_DATA")


def configure_otel_providers(
*,
enable_sensitive_data: bool | None = None,
enable_console_exporters: bool | None = None,
exporters: list[LogRecordExporter | SpanExporter | MetricExporter] | None = None,
views: list[View] | None = None,
vs_code_extension_port: int | None = None,
Expand Down Expand Up @@ -963,6 +986,8 @@ def configure_otel_providers(
Keyword Args:
enable_sensitive_data: Enable OpenTelemetry sensitive events. Overrides
the environment variable ENABLE_SENSITIVE_DATA if set. Default is None.
enable_console_exporters: Enable console exporters for traces, logs, and metrics.
Overrides the environment variable ENABLE_CONSOLE_EXPORTERS if set. Default is None.
exporters: A list of custom exporters for logs, metrics or spans, or any combination.
These will be added in addition to exporters configured via environment variables.
Default is None.
Expand Down Expand Up @@ -1051,6 +1076,8 @@ def configure_otel_providers(
settings_kwargs["env_file_encoding"] = env_file_encoding
if enable_sensitive_data is not None:
settings_kwargs["enable_sensitive_data"] = enable_sensitive_data
if enable_console_exporters is not None:
settings_kwargs["enable_console_exporters"] = enable_console_exporters
if vs_code_extension_port is not None:
settings_kwargs["vs_code_extension_port"] = vs_code_extension_port

Expand All @@ -1064,12 +1091,22 @@ def configure_otel_providers(
OBSERVABILITY_SETTINGS._resource = updated_settings._resource # type: ignore[reportPrivateUsage]
OBSERVABILITY_SETTINGS._executed_setup = False # type: ignore[reportPrivateUsage]
else:
# Update the observability settings with the provided values
# Re-read settings from current environment in case env vars were set
# after import (e.g. via load_dotenv()). Explicit parameters take precedence.
OBSERVABILITY_SETTINGS.enable_instrumentation = True
if enable_sensitive_data is not None:
OBSERVABILITY_SETTINGS.enable_sensitive_data = enable_sensitive_data
if vs_code_extension_port is not None:
OBSERVABILITY_SETTINGS.vs_code_extension_port = vs_code_extension_port
OBSERVABILITY_SETTINGS.enable_sensitive_data = (
enable_sensitive_data if enable_sensitive_data is not None else _read_bool_env("ENABLE_SENSITIVE_DATA")
)
OBSERVABILITY_SETTINGS.enable_console_exporters = (
enable_console_exporters
if enable_console_exporters is not None
else _read_bool_env("ENABLE_CONSOLE_EXPORTERS")
)
OBSERVABILITY_SETTINGS.vs_code_extension_port = (
vs_code_extension_port if vs_code_extension_port is not None else _read_int_env("VS_CODE_EXTENSION_PORT")
)
OBSERVABILITY_SETTINGS._resource = create_resource() # type: ignore[reportPrivateUsage]
OBSERVABILITY_SETTINGS._executed_setup = False # type: ignore[reportPrivateUsage]

OBSERVABILITY_SETTINGS._configure( # type: ignore[reportPrivateUsage]
additional_exporters=exporters,
Expand Down
266 changes: 266 additions & 0 deletions python/packages/core/tests/core/test_observability.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,6 +1033,272 @@ def test_enable_instrumentation_with_sensitive_data(monkeypatch):
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True


def test_enable_instrumentation_reads_env_sensitive_data(monkeypatch):
"""Test enable_instrumentation re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false")

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False

# Simulate load_dotenv() setting env var after import
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true")

observability.enable_instrumentation()
assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True


def test_configure_otel_providers_reads_env_sensitive_data(monkeypatch):
"""Test configure_otel_providers re-reads ENABLE_SENSITIVE_DATA from os.environ when not explicitly passed."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "false")
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False

# Simulate load_dotenv() setting env var after import
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true")

observability.configure_otel_providers()
assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is True


def test_configure_otel_providers_reads_env_vs_code_port(monkeypatch):
"""Test configure_otel_providers re-reads VS_CODE_EXTENSION_PORT from os.environ when not explicitly passed."""
import importlib
from unittest.mock import patch as mock_patch

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port is None

# Simulate load_dotenv() setting env var after import
monkeypatch.setenv("VS_CODE_EXTENSION_PORT", "4317")

# Mock _configure to avoid needing optional OTLP gRPC exporter dependency
with mock_patch.object(observability.OBSERVABILITY_SETTINGS, "_configure"):
observability.configure_otel_providers()
assert observability.OBSERVABILITY_SETTINGS.vs_code_extension_port == 4317


def test_configure_otel_providers_explicit_param_overrides_env(monkeypatch):
"""Test that explicit parameters to configure_otel_providers override env vars."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true")
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

# Explicit False should override the env var True
observability.configure_otel_providers(enable_sensitive_data=False)
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False


def test_enable_instrumentation_explicit_param_overrides_env(monkeypatch):
"""Test that explicit enable_sensitive_data parameter to enable_instrumentation overrides env var."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_SENSITIVE_DATA", "true")

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

# Explicit False should override the env var True
observability.enable_instrumentation(enable_sensitive_data=False)
assert observability.OBSERVABILITY_SETTINGS.enable_instrumentation is True
assert observability.OBSERVABILITY_SETTINGS.enable_sensitive_data is False


def test_enable_instrumentation_does_not_touch_console_exporters(monkeypatch):
"""Test enable_instrumentation does not modify enable_console_exporters (it is an exporter concern)."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False

# Simulate load_dotenv() setting env var after import
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")

observability.enable_instrumentation()
# enable_console_exporters is not managed by enable_instrumentation;
# it is only read by configure_otel_providers.
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False


def test_enable_instrumentation_does_not_clobber_console_exporters(monkeypatch):
"""Test enable_instrumentation does not reset enable_console_exporters set by prior configure call."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False)
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

# Set console exporters via configure_otel_providers
observability.configure_otel_providers(enable_console_exporters=True)
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True

# Calling enable_instrumentation should not clobber the value
observability.enable_instrumentation()
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True


def test_enable_instrumentation_with_sensitive_data_does_not_touch_console_exporters(monkeypatch):
"""Test enable_console_exporters is untouched even when enable_sensitive_data is explicitly passed."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
monkeypatch.delenv("ENABLE_SENSITIVE_DATA", raising=False)
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

# Set console exporters via configure_otel_providers
observability.configure_otel_providers(enable_console_exporters=True)
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True

# Calling enable_instrumentation with explicit sensitive_data should not clobber console exporters
observability.enable_instrumentation(enable_sensitive_data=True)
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True


def test_enable_instrumentation_preserves_console_exporters_after_env_removed(monkeypatch):
"""Test enable_instrumentation preserves enable_console_exporters when env var is removed after reload."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True

# Remove the env var after reload
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)

# enable_instrumentation should not reset the value
observability.enable_instrumentation()
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True


def test_configure_otel_providers_reads_env_console_exporters(monkeypatch):
"""Test configure_otel_providers re-reads ENABLE_CONSOLE_EXPORTERS from os.environ when not explicitly passed."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
monkeypatch.delenv("ENABLE_CONSOLE_EXPORTERS", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False

# Simulate load_dotenv() setting env var after import
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")

observability.configure_otel_providers()
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is True


def test_configure_otel_providers_explicit_console_exporters_overrides_env(monkeypatch):
"""Test that explicit enable_console_exporters parameter overrides the environment variable."""
import importlib

monkeypatch.setenv("ENABLE_INSTRUMENTATION", "false")
monkeypatch.setenv("ENABLE_CONSOLE_EXPORTERS", "true")
monkeypatch.delenv("VS_CODE_EXTENSION_PORT", raising=False)
for key in [
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_METRICS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
]:
monkeypatch.delenv(key, raising=False)

observability = importlib.import_module("agent_framework.observability")
importlib.reload(observability)

# Explicit False should override the env var True
observability.configure_otel_providers(enable_console_exporters=False)
assert observability.OBSERVABILITY_SETTINGS.enable_console_exporters is False


# region Test _to_otel_part content types


Expand Down
Loading