Skip to content

Commit 6ed8c1a

Browse files
Address code review feedback: add validation, improve tests, optimize env var reading
Co-authored-by: sergioescalera <8428450+sergioescalera@users.noreply.github.com>
1 parent 8a94d44 commit 6ed8c1a

4 files changed

Lines changed: 195 additions & 39 deletions

File tree

libraries/microsoft-agents-a365-observability-core/microsoft_agents_a365/observability/core/exporters/agent365_exporter.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ def __init__(
6161
self._token_resolver = token_resolver
6262
self._cluster_category = cluster_category
6363
self._use_s2s_endpoint = use_s2s_endpoint
64+
# Read domain override once at initialization
65+
self._domain_override = self._get_validated_domain_override()
6466

6567
# ------------- SpanExporter API -----------------
6668

@@ -87,9 +89,8 @@ def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
8789
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
8890

8991
# Resolve endpoint + token
90-
domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE")
91-
if domain_override:
92-
endpoint = domain_override
92+
if self._domain_override:
93+
endpoint = self._domain_override
9394
else:
9495
discovery = PowerPlatformApiDiscovery(self._cluster_category)
9596
endpoint = discovery.get_tenant_island_cluster_endpoint(tenant_id)
@@ -147,6 +148,30 @@ def shutdown(self) -> None:
147148
def force_flush(self, timeout_millis: int = 30000) -> bool:
148149
return True
149150

151+
# ------------- Helper methods -------------------
152+
153+
@staticmethod
154+
def _get_validated_domain_override() -> str | None:
155+
"""
156+
Get and validate the domain override from environment variable.
157+
158+
Returns:
159+
The validated domain override, or None if not set or invalid.
160+
"""
161+
domain_override = os.getenv("A365_OBSERVABILITY_DOMAIN_OVERRIDE", "").strip()
162+
if not domain_override:
163+
return None
164+
165+
# Basic validation: ensure domain doesn't contain protocol or path separators
166+
if "://" in domain_override or "/" in domain_override:
167+
logger.warning(
168+
f"Invalid domain override '{domain_override}': "
169+
"domain should not contain protocol (://) or path separators (/)"
170+
)
171+
return None
172+
173+
return domain_override
174+
150175
# ------------- HTTP helper ----------------------
151176

152177
@staticmethod

libraries/microsoft-agents-a365-runtime/microsoft_agents_a365/runtime/environment_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ def get_observability_authentication_scope() -> list[str]:
2424
The scope can be overridden via the A365_OBSERVABILITY_SCOPE_OVERRIDE environment variable
2525
to enable testing against pre-production environments.
2626
27+
Environment Variable:
28+
A365_OBSERVABILITY_SCOPE_OVERRIDE: Full authentication scope URL including the /.default suffix
29+
(e.g., "https://preprod.api.powerplatform.com/.default")
30+
2731
Returns:
2832
list[str]: The authentication scope for the current environment.
2933
"""
30-
override_scope = os.getenv("A365_OBSERVABILITY_SCOPE_OVERRIDE")
34+
override_scope = os.getenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", "").strip()
3135
return [override_scope] if override_scope else [PROD_OBSERVABILITY_SCOPE]
3236

3337

tests/observability/core/test_agent365_exporter.py

Lines changed: 137 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,26 @@ def setUp(self):
2020
"""Set up test fixtures."""
2121
self.mock_token_resolver = Mock()
2222
self.mock_token_resolver.return_value = "test_token_123"
23-
24-
# Don't patch the class in setUp, do it per test
23+
24+
# Store original environment variable values for cleanup
25+
self._original_domain_override = os.environ.get("A365_OBSERVABILITY_DOMAIN_OVERRIDE")
26+
27+
# Ensure no override is set by default for most tests
28+
os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None)
29+
30+
# Create default exporter for tests that don't need special setup
2531
self.exporter = _Agent365Exporter(
2632
token_resolver=self.mock_token_resolver, cluster_category="test"
2733
)
2834

35+
def tearDown(self):
36+
"""Clean up test environment."""
37+
# Restore original environment variable value
38+
if self._original_domain_override is None:
39+
os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None)
40+
else:
41+
os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = self._original_domain_override
42+
2943
def _create_mock_span(
3044
self,
3145
name: str = "test_span",
@@ -389,48 +403,49 @@ def test_export_uses_domain_override_when_env_var_set(self):
389403
"""Test that domain override is used when A365_OBSERVABILITY_DOMAIN_OVERRIDE is set."""
390404
# Arrange
391405
override_domain = "override.example.com"
392-
393406
os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = override_domain
407+
408+
# Create exporter after setting environment variable so it reads the override
409+
exporter = _Agent365Exporter(
410+
token_resolver=self.mock_token_resolver, cluster_category="test"
411+
)
412+
413+
spans = [self._create_mock_span("override_test_span")]
394414

395-
try:
396-
spans = [self._create_mock_span("override_test_span")]
397-
398-
# Mock the PowerPlatformApiDiscovery class (should not be called when override is set)
399-
with patch(
400-
"microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery"
401-
) as mock_discovery_class:
402-
# Mock the _post_with_retries method
403-
with patch.object(
404-
self.exporter, "_post_with_retries", return_value=True
405-
) as mock_post:
406-
# Act
407-
result = self.exporter.export(spans)
408-
409-
# Assert
410-
self.assertEqual(result, SpanExportResult.SUCCESS)
411-
mock_post.assert_called_once()
415+
# Mock the PowerPlatformApiDiscovery class (should not be called when override is set)
416+
with patch(
417+
"microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery"
418+
) as mock_discovery_class:
419+
# Mock the _post_with_retries method
420+
with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post:
421+
# Act
422+
result = exporter.export(spans)
412423

413-
# Verify the call arguments - should use override domain
414-
args, kwargs = mock_post.call_args
415-
url, body, headers = args
424+
# Assert
425+
self.assertEqual(result, SpanExportResult.SUCCESS)
426+
mock_post.assert_called_once()
416427

417-
self.assertIn(override_domain, url)
418-
self.assertIn("/maven/agent365/agents/test-agent-456/traces", url)
428+
# Verify the call arguments - should use override domain with complete URL
429+
args, kwargs = mock_post.call_args
430+
url, body, headers = args
419431

420-
# Verify PowerPlatformApiDiscovery was not instantiated
421-
mock_discovery_class.assert_not_called()
432+
expected_url = f"https://{override_domain}/maven/agent365/agents/test-agent-456/traces?api-version=1"
433+
self.assertEqual(url, expected_url)
422434

423-
finally:
424-
# Cleanup
425-
del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"]
435+
# Verify PowerPlatformApiDiscovery was not instantiated
436+
mock_discovery_class.assert_not_called()
426437

427438
def test_export_uses_default_domain_when_no_override(self):
428439
"""Test that default domain resolution is used when no override is set."""
429440
# Arrange
430441
# Ensure override is not set
431-
if "A365_OBSERVABILITY_DOMAIN_OVERRIDE" in os.environ:
432-
del os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"]
433-
442+
os.environ.pop("A365_OBSERVABILITY_DOMAIN_OVERRIDE", None)
443+
444+
# Create exporter after clearing environment variable
445+
exporter = _Agent365Exporter(
446+
token_resolver=self.mock_token_resolver, cluster_category="test"
447+
)
448+
434449
spans = [self._create_mock_span("default_domain_span")]
435450

436451
# Mock the PowerPlatformApiDiscovery class
@@ -442,9 +457,9 @@ def test_export_uses_default_domain_when_no_override(self):
442457
mock_discovery_class.return_value = mock_discovery
443458

444459
# Mock the _post_with_retries method
445-
with patch.object(self.exporter, "_post_with_retries", return_value=True) as mock_post:
460+
with patch.object(exporter, "_post_with_retries", return_value=True) as mock_post:
446461
# Act
447-
result = self.exporter.export(spans)
462+
result = exporter.export(spans)
448463

449464
# Assert
450465
self.assertEqual(result, SpanExportResult.SUCCESS)
@@ -463,6 +478,93 @@ def test_export_uses_default_domain_when_no_override(self):
463478
"test-tenant-123"
464479
)
465480

481+
def test_export_ignores_empty_domain_override(self):
482+
"""Test that empty or whitespace-only domain override is ignored."""
483+
# Arrange
484+
os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = " " # whitespace only
485+
486+
# Create exporter after setting environment variable
487+
exporter = _Agent365Exporter(
488+
token_resolver=self.mock_token_resolver, cluster_category="test"
489+
)
490+
491+
spans = [self._create_mock_span("test_span")]
492+
493+
# Mock the PowerPlatformApiDiscovery class (should be called since override is invalid)
494+
with patch(
495+
"microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery"
496+
) as mock_discovery_class:
497+
mock_discovery = Mock()
498+
mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com"
499+
mock_discovery_class.return_value = mock_discovery
500+
501+
with patch.object(exporter, "_post_with_retries", return_value=True):
502+
# Act
503+
result = exporter.export(spans)
504+
505+
# Assert
506+
self.assertEqual(result, SpanExportResult.SUCCESS)
507+
# Verify PowerPlatformApiDiscovery was called (override was ignored)
508+
mock_discovery_class.assert_called_once_with("test")
509+
510+
def test_export_ignores_invalid_domain_with_protocol(self):
511+
"""Test that domain override containing protocol is ignored."""
512+
# Arrange
513+
os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "https://invalid.example.com"
514+
515+
# Create exporter after setting environment variable
516+
exporter = _Agent365Exporter(
517+
token_resolver=self.mock_token_resolver, cluster_category="test"
518+
)
519+
520+
spans = [self._create_mock_span("test_span")]
521+
522+
# Mock the PowerPlatformApiDiscovery class (should be called since override is invalid)
523+
with patch(
524+
"microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery"
525+
) as mock_discovery_class:
526+
mock_discovery = Mock()
527+
mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com"
528+
mock_discovery_class.return_value = mock_discovery
529+
530+
with patch.object(exporter, "_post_with_retries", return_value=True):
531+
# Act
532+
result = exporter.export(spans)
533+
534+
# Assert
535+
self.assertEqual(result, SpanExportResult.SUCCESS)
536+
# Verify PowerPlatformApiDiscovery was called (override was ignored)
537+
mock_discovery_class.assert_called_once_with("test")
538+
539+
def test_export_ignores_invalid_domain_with_path(self):
540+
"""Test that domain override containing path separator is ignored."""
541+
# Arrange
542+
os.environ["A365_OBSERVABILITY_DOMAIN_OVERRIDE"] = "invalid.example.com/path"
543+
544+
# Create exporter after setting environment variable
545+
exporter = _Agent365Exporter(
546+
token_resolver=self.mock_token_resolver, cluster_category="test"
547+
)
548+
549+
spans = [self._create_mock_span("test_span")]
550+
551+
# Mock the PowerPlatformApiDiscovery class (should be called since override is invalid)
552+
with patch(
553+
"microsoft_agents_a365.observability.core.exporters.agent365_exporter.PowerPlatformApiDiscovery"
554+
) as mock_discovery_class:
555+
mock_discovery = Mock()
556+
mock_discovery.get_tenant_island_cluster_endpoint.return_value = "default-endpoint.com"
557+
mock_discovery_class.return_value = mock_discovery
558+
559+
with patch.object(exporter, "_post_with_retries", return_value=True):
560+
# Act
561+
result = exporter.export(spans)
562+
563+
# Assert
564+
self.assertEqual(result, SpanExportResult.SUCCESS)
565+
# Verify PowerPlatformApiDiscovery was called (override was ignored)
566+
mock_discovery_class.assert_called_once_with("test")
567+
466568

467569
if __name__ == "__main__":
468570
unittest.main()

tests/runtime/test_environment_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,31 @@ def test_get_observability_authentication_scope_with_override(monkeypatch):
2727
assert result == [override_scope]
2828

2929

30+
def test_get_observability_authentication_scope_ignores_empty_override(monkeypatch):
31+
"""Test get_observability_authentication_scope ignores empty string override."""
32+
monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", "")
33+
34+
result = get_observability_authentication_scope()
35+
assert result == [PROD_OBSERVABILITY_SCOPE]
36+
37+
38+
def test_get_observability_authentication_scope_ignores_whitespace_override(monkeypatch):
39+
"""Test get_observability_authentication_scope ignores whitespace-only override."""
40+
monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", " ")
41+
42+
result = get_observability_authentication_scope()
43+
assert result == [PROD_OBSERVABILITY_SCOPE]
44+
45+
46+
def test_get_observability_authentication_scope_trims_whitespace(monkeypatch):
47+
"""Test get_observability_authentication_scope trims whitespace from override."""
48+
override_scope = " https://override.example.com/.default "
49+
monkeypatch.setenv("A365_OBSERVABILITY_SCOPE_OVERRIDE", override_scope)
50+
51+
result = get_observability_authentication_scope()
52+
assert result == [override_scope.strip()]
53+
54+
3055
@pytest.mark.parametrize(
3156
"env_value,expected",
3257
[

0 commit comments

Comments
 (0)