Skip to content
Draft
6 changes: 5 additions & 1 deletion src/Observability/Hosting/Caching/AgenticTokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ public void RegisterObservability(string agentId, string tenantId, AgenticTokenS
/// <returns>
/// The observability token if available; otherwise, <c>null</c>.
/// </returns>
public async Task<string?> GetObservabilityToken(string agentId, string tenantId)
public Task<string?> GetObservabilityToken(string agentId, string tenantId)
=> GetObservabilityToken(agentId, tenantId, CancellationToken.None);

/// <inheritdoc/>
public async Task<string?> GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken)
{
if (!_map.TryGetValue($"{agentId}:{tenantId}", out var entry))
return null;
Expand Down
6 changes: 6 additions & 0 deletions src/Observability/Hosting/Caching/IExporterTokenCache.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Agents.A365.Observability.Hosting.Caching
Expand All @@ -18,5 +19,10 @@ public interface IExporterTokenCache<T> where T : class
/// Returns an observability token (cached inside the credential) or null on failure/not registered.
/// </summary>
Task<string?> GetObservabilityToken(string agentId, string tenantId);

/// <summary>
/// Returns an observability token (cached inside the credential) or null on failure/not registered, with cancellation support.
/// </summary>
Task<string?> GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken);
}
}
14 changes: 9 additions & 5 deletions src/Observability/Hosting/Caching/ServiceTokenCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,19 @@ public void RegisterObservability(string agentId, string tenantId, string token,
/// <param name="agentId">The agent identifier.</param>
/// <param name="tenantId">The tenant identifier.</param>
/// <returns>The observability token if valid; otherwise, null.</returns>
public async Task<string?> GetObservabilityToken(string agentId, string tenantId)
public Task<string?> GetObservabilityToken(string agentId, string tenantId)
=> GetObservabilityToken(agentId, tenantId, CancellationToken.None);

/// <inheritdoc/>
public Task<string?> GetObservabilityToken(string agentId, string tenantId, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(agentId) || string.IsNullOrWhiteSpace(tenantId))
return null;
return Task.FromResult<string?>(null);

var key = GetKey(agentId, tenantId);

if (!_map.TryGetValue(key, out var entry))
return null;
return Task.FromResult<string?>(null);

// Check if token has expired
if (DateTimeOffset.UtcNow >= entry.ExpiresAt)
Expand All @@ -140,10 +144,10 @@ public void RegisterObservability(string agentId, string tenantId, string token,
{
removedEntry.ClearToken();
}
return null;
return Task.FromResult<string?>(null);
}

return await Task.FromResult(entry.Token).ConfigureAwait(false);
return Task.FromResult(entry.Token);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static IServiceCollection AddAgenticTracingExporter(this IServiceCollecti
return new Agent365ExporterOptions
{
ClusterCategory = clusterCategory ?? "production",
TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId)
TokenResolver = (agentId, tenantId, ct) => cache.GetObservabilityToken(agentId, tenantId, ct)
};
});

Expand All @@ -51,7 +51,7 @@ public static IServiceCollection AddServiceTracingExporter(this IServiceCollecti
return new Agent365ExporterOptions
{
ClusterCategory = clusterCategory ?? "production",
TokenResolver = async (agentId, tenantId) => await cache.GetObservabilityToken(agentId, tenantId).ConfigureAwait(false),
TokenResolver = (agentId, tenantId, ct) => cache.GetObservabilityToken(agentId, tenantId, ct),
UseS2SEndpoint = true // Service-to-service uses S2S endpoint
};
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public override ExportResult Export(in Batch<Activity> batch)
groups: groups,
resource: _resource,
options: _options,
tokenResolver: (agentId, tenantId) => _options.TokenResolver!(agentId, tenantId),
tokenResolver: (agentId, tenantId, ct) => _options.TokenResolver!(agentId, tenantId, ct),
sendAsync: request => _httpClient.SendAsync(request)
).GetAwaiter().GetResult();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ await _core.ExportBatchCoreAsync(
groups: groups,
resource: this._resource,
options: this._options,
tokenResolver: (agentId, tenantId) => this._options.TokenResolver!(agentId, tenantId),
sendAsync: request => this._httpClient.SendAsync(request, cancellationToken)
tokenResolver: (agentId, tenantId, ct) => this._options.TokenResolver!(agentId, tenantId, ct),
sendAsync: request => this._httpClient.SendAsync(request, cancellationToken),
cancellationToken: cancellationToken
).ConfigureAwait(false);
}
catch (OperationCanceledException)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
Expand Down Expand Up @@ -127,13 +128,15 @@ public string BuildRequestUri(string endpoint, string endpointPath)
/// <param name="options"></param>
/// <param name="tokenResolver"></param>
/// <param name="sendAsync"></param>
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns></returns>
public async Task<ExportResult> ExportBatchCoreAsync(
IEnumerable<(string TenantId, string AgentId, List<Activity> Activities)> groups,
Resource resource,
Agent365ExporterOptions options,
Func<string, string, Task<string?>> tokenResolver,
Func<HttpRequestMessage, Task<HttpResponseMessage>> sendAsync)
Func<string, string, CancellationToken, Task<string?>> tokenResolver,
Func<HttpRequestMessage, Task<HttpResponseMessage>> sendAsync,
CancellationToken cancellationToken = default)
{
foreach (var g in groups)
{
Expand All @@ -157,7 +160,7 @@ public async Task<ExportResult> ExportBatchCoreAsync(
string? token = null;
try
{
token = await tokenResolver(agentId, tenantId).ConfigureAwait(false);
token = await tokenResolver(agentId, tenantId, cancellationToken).ConfigureAwait(false);
this._logger?.LogDebug("Agent365ExporterCore: Obtained token for agent {AgentId} tenant {TenantId}.", agentId, tenantId);
}
catch (Exception ex)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
Expand All @@ -10,7 +11,7 @@ namespace Microsoft.Agents.A365.Observability.Runtime.Tracing.Exporters
/// Must be fast and non-blocking (use internal caching elsewhere).
/// Return null/empty to omit the Authorization header.
/// </summary>
public delegate Task<string?> AsyncAuthTokenResolver(string agentId, string tenantId);
public delegate Task<string?> AsyncAuthTokenResolver(string agentId, string tenantId, CancellationToken cancellationToken = default);

Comment on lines +14 to 15
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this public delegate signature by adding a CancellationToken parameter is a binary-breaking API change for consumers compiled against previous versions (delegate Invoke signature changes). If compatibility is needed, consider introducing a new delegate type/property (or overload pattern) while keeping the existing delegate for older consumers.

Copilot uses AI. Check for mistakes.
/// <summary>
/// Delegate used by the exporter to resolve the endpoint host or URL for a given tenant id.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void AddServiceTracingExporter_TokenResolver_CanBeCalled()
var options = serviceProvider.GetRequiredService<Agent365ExporterOptions>();

// Act
var token = options.TokenResolver!("test-agent", "test-tenant");
var token = options.TokenResolver!("test-agent", "test-tenant", default);

// Assert
// Token resolver should not throw (actual token retrieval logic is in the cache)
Expand All @@ -139,8 +139,7 @@ public void AddAgenticTracingExporter_TokenResolver_CanBeCalled()
var options = serviceProvider.GetRequiredService<Agent365ExporterOptions>();

// Act
var token = options.TokenResolver!("test-agent", "test-tenant");

var token = options.TokenResolver!("test-agent", "test-tenant", default);
// Assert
// Token resolver should not throw (actual token retrieval logic is in the cache)
// This just verifies the resolver is wired up
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public void AddA365Tracing_WithOpenTelemetryBuilderTrue_AndExporterEnabled_Regis
builder.Services.AddSingleton<Agent365ExporterOptions>(sp => new Agent365ExporterOptions
{
UseS2SEndpoint = false,
TokenResolver = (_, _) => Task.FromResult<string?>("test-token")
TokenResolver = (_, _, _) => Task.FromResult<string?>("test-token")
});
});
webHostBuilder.UseStartup<MinimalStartup>();
Expand Down Expand Up @@ -160,7 +160,7 @@ public void AddA365Tracing_IHostBuilder_WithOpenTelemetryBuilderTrue_AndExporter
builder.Services.AddSingleton<Agent365ExporterOptions>(_ => new Agent365ExporterOptions
{
UseS2SEndpoint = false,
TokenResolver = (_, _) => Task.FromResult<string?>("test-token")
TokenResolver = (_, _, _) => Task.FromResult<string?>("test-token")
});
});

Expand Down
Loading
Loading