-
Notifications
You must be signed in to change notification settings - Fork 8
[Observability] Use custom domain for A365 service #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds support for optionally routing observability traces to a custom Agent365 domain instead of the default Power Platform API (PPAPI) gateway-derived tenant island endpoints. The feature is controlled by a new UseCustomDomain flag in Agent365ExporterOptions, defaulting to false to preserve existing behavior.
Key changes include:
- Introduced
Agent365EndpointDiscoveryclass to resolve custom domain endpoints based on cluster category - Extended
BuildEndpointPathto support custom domain routing with simplified path structure (/agents/{agentId}/traces) - Added
x-ms-tenant-idheader when using custom domain (tenant is encoded in URL for PPAPI)
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
Agent365EndpointDiscovery.cs |
New class providing endpoint discovery for custom Agent365 domain across different cluster categories (preprod, production) |
Agent365EndpointDiscoveryTest.cs |
Comprehensive test coverage for endpoint discovery including mapping verification, case-insensitivity, and default behavior |
Agent365ExporterOptions.cs |
Added UseCustomDomain property with documentation explaining the feature and default behavior |
Agent365ExporterCore.cs |
Updated BuildEndpointPath signature to accept useCustomDomain parameter; modified export logic to conditionally use custom domain endpoint and add tenant ID header |
Agent365ExporterTests.cs |
Added tests for custom domain routing behavior, endpoint path construction, and tenant header validation; removed [TestMethod] attribute from one existing test (appears unintentional) |
src/Observability/Runtime/Tracing/Exporters/Agent365ExporterOptions.cs
Outdated
Show resolved
Hide resolved
src/Observability/Runtime/Tracing/Exporters/Agent365ExporterCore.cs
Outdated
Show resolved
Hide resolved
...Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs
Show resolved
Hide resolved
...Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs
Show resolved
Hide resolved
juliomenendez
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use a setting like we do for UseAgent365Exporter for this? With that in place, should we be able to remove the arguments from the methods we have here and just read the setting?
juliomenendez
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scope for which the token is generated also needs to change to api://9b975845-388f-4429-889e-eab1ef63949c/.default
Since Agent365ExporterCore is static, passing IConfiguration to it will follow the same anti-pattern that we have for the logger. I have a separate PR to address it, will include IConfiguration. Until then reading this from an environment variable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
...ts/Microsoft.Agents.A365.Observability.Runtime.Tests/Common/Agent365EndpointDiscoveryTest.cs
Show resolved
Hide resolved
...Microsoft.Agents.A365.Observability.Runtime.Tests/Tracing/Exporters/Agent365ExporterTests.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
| return EnvironmentUtils.IsCustomDomainEnabled(configuration: this._configuration) | ||
| ? $"/agents/{agentId}/traces" | ||
| : useS2SEndpoint | ||
| ? $"/maven/agent365/service/agents/{agentId}/traces" | ||
| : $"/maven/agent365/agents/{agentId}/traces"; |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IsCustomDomainEnabled method is called multiple times within the export flow (in BuildEndpointPath, ExportBatchCoreAsync for endpoint determination, and again for header addition). Consider caching this value at the constructor level to avoid repeated configuration lookups:
private readonly bool _useCustomDomain;
public Agent365ExporterCore(...)
{
// ...
_useCustomDomain = EnvironmentUtils.IsCustomDomainEnabled(configuration);
}|
|
||
|
|
||
| [TestClass] | ||
| public class Agent365EndpointDiscoveryTests |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The class name should be Agent365EndpointDiscoveryTests (plural) to match the naming convention used elsewhere in the test suite (e.g., Agent365ExporterTests, EnvironmentUtilsTests).
| @@ -1,4 +1,4 @@ | |||
| using FluentAssertions; | |||
| using FluentAssertions; | |||
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing required Microsoft copyright header. Add the following at the top of the file:
// ------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// ------------------------------------------------------------------------------| public Agent365ExporterCore(ExportFormatter formatter, ILogger<Agent365ExporterCore> logger, IConfiguration? configuration) | ||
| { | ||
| _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); | ||
| _logger = logger ?? NullLogger<Agent365ExporterCore>.Instance; | ||
| _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The configuration parameter is marked as nullable but throws ArgumentNullException if null. This creates an inconsistent API contract. Consider one of these approaches:
- Make it non-nullable (
IConfiguration configuration) if it's required - Allow null and use
NullConfigurationor handle gracefully (e.g.,_configuration = configuration ?? new ConfigurationBuilder().Build())
The inconsistency is especially problematic since line 80 in ObservabilityTracerProviderBuilderExtensions.cs passes potentially null configuration to this constructor.
| public Agent365ExporterCore(ExportFormatter formatter, ILogger<Agent365ExporterCore> logger, IConfiguration? configuration) | |
| { | |
| _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); | |
| _logger = logger ?? NullLogger<Agent365ExporterCore>.Instance; | |
| _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); | |
| public Agent365ExporterCore(ExportFormatter formatter, ILogger<Agent365ExporterCore> logger, IConfiguration configuration) | |
| { | |
| _formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); | |
| _logger = logger ?? NullLogger<Agent365ExporterCore>.Instance; | |
| _configuration = configuration; |
| _configuration[name] = enabled ? bool.TrueString : bool.FalseString; | ||
| return new ConfigReset(name, prev); | ||
| } | ||
|
|
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing [TestMethod] attribute. This test method will not be executed by the test runner without it.
| [TestMethod] |
|
|
||
| /// <summary> | ||
| /// Returns true if the custom domain feature is enabled. | ||
| /// </summary> |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The XML documentation should include the parameter description. Add:
/// <param name="configuration">The configuration instance to check for custom domain settings. If null, returns false.</param>
/// <returns>True if custom domain is enabled; otherwise, false.</returns>| /// </summary> | |
| /// </summary> | |
| /// <param name="configuration">The configuration instance to check for custom domain settings. If null, returns false.</param> | |
| /// <returns>True if custom domain is enabled; otherwise, false.</returns> |
| { | ||
| // clusterCategory is ignored; always returns production scope | ||
| return new[] { ProdObservabilityScope }; | ||
| return EnvironmentUtils.IsCustomDomainEnabled(configuration: configuration) ? new[] { Agent365EndpointProdObservabilityScope } : new[] { ProdObservabilityScope }; |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is very long and hard to read. Consider breaking it into multiple lines for better maintainability:
return EnvironmentUtils.IsCustomDomainEnabled(configuration: configuration)
? new[] { Agent365EndpointProdObservabilityScope }
: new[] { ProdObservabilityScope };| return EnvironmentUtils.IsCustomDomainEnabled(configuration: configuration) ? new[] { Agent365EndpointProdObservabilityScope } : new[] { ProdObservabilityScope }; | |
| string[] scope; | |
| if (EnvironmentUtils.IsCustomDomainEnabled(configuration: configuration)) | |
| { | |
| scope = new[] { Agent365EndpointProdObservabilityScope }; | |
| } | |
| else | |
| { | |
| scope = new[] { ProdObservabilityScope }; | |
| } | |
| return scope; |
| private static readonly IConfigurationRoot _configuration = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?>()) | ||
| .Build(); | ||
| private static readonly Agent365ExporterCore _agent365ExporterCore = new Agent365ExporterCore(new ExportFormatter(NullLogger<ExportFormatter>.Instance), NullLogger<Agent365ExporterCore>.Instance, _configuration); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using a static shared configuration across all tests can cause test flakiness when tests run in parallel. Consider using [TestInitialize] and [TestCleanup] methods to create and reset configuration per test, or use the [DoNotParallelize] attribute on the test class if parallel execution is causing issues.
| if (configuration != null && configuration["EnableAgent365CustomDomain"] != null) | ||
| { | ||
| string enabled = configuration["EnableAgent365CustomDomain"]!; | ||
| return enabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); |
Copilot
AI
Dec 3, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The null check configuration["EnableAgent365CustomDomain"] != null is redundant. The indexer returns null when the key doesn't exist, so you can simplify this to:
if (configuration != null)
{
string? enabled = configuration["EnableAgent365CustomDomain"];
if (enabled != null)
{
return enabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
}
}
return false;| if (configuration != null && configuration["EnableAgent365CustomDomain"] != null) | |
| { | |
| string enabled = configuration["EnableAgent365CustomDomain"]!; | |
| return enabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); | |
| if (configuration != null) | |
| { | |
| string? enabled = configuration["EnableAgent365CustomDomain"]; | |
| if (enabled != null) | |
| { | |
| return enabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase); | |
| } |
| /// </summary> | ||
| public static bool IsCustomDomainEnabled(IConfiguration? configuration) | ||
| { | ||
| if (configuration != null && configuration["EnableAgent365CustomDomain"] != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this configuration to be set by the agent developer?
If we need a fallback we should handle it on the exporter right?
let me know if I'm missing something
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want to continue using the ppapi endpoint for backward compatibility for now. We want to use the custom domain just for our E2E testing for now. Once we are ready to switch all our clients over, we wont need this configuration at all.
Does this answer your question?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the code default to the new endpoint? the tenant endpoint should be a fallback right?
If not, agent developers will need to add a env variable and then remove it later.
| /// </summary> | ||
| /// <param name="configuration">The configuration instance.</param> | ||
| /// <param name="force">When true, re-initializes even if already initialized.</param> | ||
| public static void Initialize(IConfiguration? configuration, bool force = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead forcing this to be static and use this Initialize method, do you think it would make sense to convert this class to instantiable? Meaning we make the methods non-static and do this IConfiguration in the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can but it would be a breaking change for consumers. We will register and they will have to resolve EnvironmentUtils from their service collection. Or they create an instance of EnvironmentUtils and supply IConfiguration. All this temporarily (because eventually we just want to use the custom domain, no need of the configuration at all, right?)
Whereas today they simply invoke EnvironmentUtils.GetAuthenticationScope()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
src/Observability/Runtime/Tracing/Exporters/ObservabilityTracerProviderBuilderExtensions.cs:80
- This assignment to configuration is useless, since its value is never read.
var configuration = serviceProvider.GetService<Microsoft.Extensions.Configuration.IConfiguration>();
| /// </summary> | ||
| public sealed class Agent365EndpointDiscovery | ||
| { | ||
| private readonly string clusterCategory; |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The field name clusterCategory should follow C# naming conventions and use PascalCase with an underscore prefix for private fields (e.g., _clusterCategory), consistent with the naming pattern used elsewhere in the codebase.
| var _configuration = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| { "EnableAgent365CustomDomain", enabled ? bool.TrueString : bool.FalseString } | ||
| }) | ||
| .Build(); | ||
| EnvironmentUtils.Initialize(configuration: _configuration, force: true); |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The local variable _configuration uses an underscore prefix which is typically reserved for class-level fields. Consider renaming to configuration to follow standard C# naming conventions for local variables.
| var _configuration = new ConfigurationBuilder() | |
| .AddInMemoryCollection(new Dictionary<string, string?> | |
| { | |
| { "EnableAgent365CustomDomain", enabled ? bool.TrueString : bool.FalseString } | |
| }) | |
| .Build(); | |
| EnvironmentUtils.Initialize(configuration: _configuration, force: true); | |
| var configuration = new ConfigurationBuilder() | |
| .AddInMemoryCollection(new Dictionary<string, string?> | |
| { | |
| { "EnableAgent365CustomDomain", enabled ? bool.TrueString : bool.FalseString } | |
| }) | |
| .Build(); | |
| EnvironmentUtils.Initialize(configuration: configuration, force: true); |
| var configuration = serviceProvider.GetService<Microsoft.Extensions.Configuration.IConfiguration>(); | ||
|
|
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The configuration variable is retrieved from the service provider but never used. If this was intended to initialize EnvironmentUtils, it should be done here. Otherwise, this variable should be removed to avoid confusion.
| var configuration = serviceProvider.GetService<Microsoft.Extensions.Configuration.IConfiguration>(); |
| Agent365ExporterTests.SetCustomDomain(true); | ||
| var path = Agent365ExporterTests._agent365ExporterCore.BuildEndpointPath("agent-123", useS2SEndpoint: true); | ||
| path.Should().Be("/agents/agent-123/traces"); |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test modifies global static state via SetCustomDomain(), which can cause test isolation issues if tests run in parallel or if subsequent tests don't reset the state. Consider:
- Using a
[TestCleanup]method to reset state after each test - Adding test execution order attributes or making tests serial
- Refactoring
EnvironmentUtilsto allow dependency injection for better testability
| tracerProviderBuilder.Build(); | ||
| } | ||
|
|
||
| EnvironmentUtils.Initialize(configuration: this.Configuration); |
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null reference exception: this.Configuration can be null (as indicated by the nullable type on line 30), but EnvironmentUtils.Initialize() throws ArgumentNullException if configuration is null (line 68-71 in EnvironmentUtils.cs).
Either:
- Add a null check before calling Initialize, or
- Pass a default empty configuration if Configuration is null, or
- Make Configuration non-nullable if it's always required
| EnvironmentUtils.Initialize(configuration: this.Configuration); | |
| EnvironmentUtils.Initialize(configuration: this.Configuration ?? new ConfigurationBuilder().Build()); |
| .Build(); | ||
| EnvironmentUtils.Initialize(configuration: _configuration, force: true); | ||
| } | ||
|
|
Copilot
AI
Dec 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test method is missing the [TestMethod] attribute, which means it will not be executed by the test runner. Add the attribute before the method declaration.
| [TestMethod] |
Optionally (using Agent365ExporterOptions.UseCustomDomain) export traces to service hosted at custom domain.