Skip to content

Conversation

@threddy
Copy link
Contributor

@threddy threddy commented Dec 1, 2025

Optionally (using Agent365ExporterOptions.UseCustomDomain) export traces to service hosted at custom domain.

@threddy threddy requested a review from a team as a code owner December 1, 2025 22:59
Copilot AI review requested due to automatic review settings December 1, 2025 22:59
Copy link
Contributor

Copilot AI left a 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 Agent365EndpointDiscovery class to resolve custom domain endpoints based on cluster category
  • Extended BuildEndpointPath to support custom domain routing with simplified path structure (/agents/{agentId}/traces)
  • Added x-ms-tenant-id header 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)

Copy link
Contributor

@juliomenendez juliomenendez left a 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?

Copy link
Contributor

@juliomenendez juliomenendez left a 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

@threddy
Copy link
Contributor Author

threddy commented Dec 2, 2025

UseAgent365Exporter

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

Copilot AI review requested due to automatic review settings December 2, 2025 00:58
Copy link
Contributor

Copilot AI left a 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.

fpfp100
fpfp100 previously approved these changes Dec 2, 2025
Copilot AI review requested due to automatic review settings December 3, 2025 22:42
Copy link
Contributor

Copilot AI left a 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.

Comment on lines 94 to 98
return EnvironmentUtils.IsCustomDomainEnabled(configuration: this._configuration)
? $"/agents/{agentId}/traces"
: useS2SEndpoint
? $"/maven/agent365/service/agents/{agentId}/traces"
: $"/maven/agent365/agents/{agentId}/traces";
Copy link

Copilot AI Dec 3, 2025

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);
}

Copilot uses AI. Check for mistakes.


[TestClass]
public class Agent365EndpointDiscoveryTests
Copy link

Copilot AI Dec 3, 2025

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).

Copilot uses AI. Check for mistakes.
@@ -1,4 +1,4 @@
using FluentAssertions;
using FluentAssertions;
Copy link

Copilot AI Dec 3, 2025

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.
// ------------------------------------------------------------------------------

Copilot generated this review using guidance from repository custom instructions.
Comment on lines 41 to 45
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));
Copy link

Copilot AI Dec 3, 2025

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:

  1. Make it non-nullable (IConfiguration configuration) if it's required
  2. Allow null and use NullConfiguration or 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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
_configuration[name] = enabled ? bool.TrueString : bool.FalseString;
return new ConfigReset(name, prev);
}

Copy link

Copilot AI Dec 3, 2025

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.

Suggested change
[TestMethod]

Copilot uses AI. Check for mistakes.

/// <summary>
/// Returns true if the custom domain feature is enabled.
/// </summary>
Copy link

Copilot AI Dec 3, 2025

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>
Suggested change
/// </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>

Copilot uses AI. Check for mistakes.
{
// clusterCategory is ignored; always returns production scope
return new[] { ProdObservabilityScope };
return EnvironmentUtils.IsCustomDomainEnabled(configuration: configuration) ? new[] { Agent365EndpointProdObservabilityScope } : new[] { ProdObservabilityScope };
Copy link

Copilot AI Dec 3, 2025

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 };
Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines 96 to 99
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);
Copy link

Copilot AI Dec 3, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 65 to 68
if (configuration != null && configuration["EnableAgent365CustomDomain"] != null)
{
string enabled = configuration["EnableAgent365CustomDomain"]!;
return enabled.Equals(bool.TrueString, StringComparison.OrdinalIgnoreCase);
Copy link

Copilot AI Dec 3, 2025

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;
Suggested change
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);
}

Copilot uses AI. Check for mistakes.
/// </summary>
public static bool IsCustomDomainEnabled(IConfiguration? configuration)
{
if (configuration != null && configuration["EnableAgent365CustomDomain"] != null)

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

Copy link
Contributor Author

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?

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)
Copy link
Contributor

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?

Copy link
Contributor Author

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()

Copilot AI review requested due to automatic review settings December 5, 2025 19:43
Copy link
Contributor

Copilot AI left a 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;
Copy link

Copilot AI Dec 5, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +124
var _configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "EnableAgent365CustomDomain", enabled ? bool.TrueString : bool.FalseString }
})
.Build();
EnvironmentUtils.Initialize(configuration: _configuration, force: true);
Copy link

Copilot AI Dec 5, 2025

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +81
var configuration = serviceProvider.GetService<Microsoft.Extensions.Configuration.IConfiguration>();

Copy link

Copilot AI Dec 5, 2025

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.

Suggested change
var configuration = serviceProvider.GetService<Microsoft.Extensions.Configuration.IConfiguration>();

Copilot uses AI. Check for mistakes.
Comment on lines +1033 to +1035
Agent365ExporterTests.SetCustomDomain(true);
var path = Agent365ExporterTests._agent365ExporterCore.BuildEndpointPath("agent-123", useS2SEndpoint: true);
path.Should().Be("/agents/agent-123/traces");
Copy link

Copilot AI Dec 5, 2025

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:

  1. Using a [TestCleanup] method to reset state after each test
  2. Adding test execution order attributes or making tests serial
  3. Refactoring EnvironmentUtils to allow dependency injection for better testability

Copilot uses AI. Check for mistakes.
tracerProviderBuilder.Build();
}

EnvironmentUtils.Initialize(configuration: this.Configuration);
Copy link

Copilot AI Dec 5, 2025

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:

  1. Add a null check before calling Initialize, or
  2. Pass a default empty configuration if Configuration is null, or
  3. Make Configuration non-nullable if it's always required
Suggested change
EnvironmentUtils.Initialize(configuration: this.Configuration);
EnvironmentUtils.Initialize(configuration: this.Configuration ?? new ConfigurationBuilder().Build());

Copilot uses AI. Check for mistakes.
.Build();
EnvironmentUtils.Initialize(configuration: _configuration, force: true);
}

Copy link

Copilot AI Dec 5, 2025

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.

Suggested change
[TestMethod]

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants