Set ApplicationName on CosmosClientOptions for UserAgent telemetry (#6481)

Added CosmosOptionsHelper (in Microsoft.Agents.AI.CosmosNoSql namespace)
that sets CosmosClientOptions.ApplicationName per component, producing
wire-visible UserAgent suffixes:

- CosmosChatHistoryProvider: Microsoft.Agents.CosmosNoSql.ChatHistory/{version}
- CosmosCheckpointStore: Microsoft.Agents.CosmosNoSql.Checkpoint/{version}

This ensures Cosmos DB requests from the Agent Framework are identifiable
in telemetry, enabling usage tracking and diagnostics queries that can
distinguish between chat history and checkpoint workloads.

Addressed review feedback:
- Truncates ApplicationName to 64 chars (Cosmos SDK max length)
- Moved helper to Microsoft.Agents.AI.CosmosNoSql namespace (scoped ownership)
- Uses StringComparison.Ordinal for IndexOf call

When users provide their own CosmosClient instance, the ApplicationName
is not overridden - users retain full control.

Co-authored-by: TheovanKraay <TheovanKraay@users.noreply.github.com>
This commit is contained in:
Theo van Kraay
2026-06-12 17:32:07 +01:00
committed by GitHub
Unverified
parent 5e830f4dc9
commit 0f483fa968
4 changed files with 165 additions and 8 deletions
@@ -8,6 +8,7 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
@@ -126,6 +127,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
Throw.IfNull(stateInitializer),
stateKey ?? this.GetType().Name);
this._cosmosClient = Throw.IfNull(cosmosClient);
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosChatHistoryProvider));
this.DatabaseId = Throw.IfNullOrWhitespace(databaseId);
this.ContainerId = Throw.IfNullOrWhitespace(containerId);
this._container = this._cosmosClient.GetContainer(databaseId, containerId);
@@ -157,7 +159,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
@@ -185,7 +187,7 @@ public sealed class CosmosChatHistoryProvider : ChatHistoryProvider, IDisposable
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
{
}
@@ -7,6 +7,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Core;
using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;
using Microsoft.Shared.Diagnostics;
using Newtonsoft.Json;
@@ -37,7 +38,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string connectionString, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions();
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));
this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(connectionString), cosmosClientOptions);
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
@@ -55,12 +56,10 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
{
var cosmosClientOptions = new CosmosClientOptions
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));
cosmosClientOptions.SerializerOptions = new CosmosSerializationOptions
{
SerializerOptions = new CosmosSerializationOptions
{
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
}
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
};
this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), cosmosClientOptions);
@@ -79,6 +78,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
public CosmosCheckpointStore(CosmosClient cosmosClient, string databaseId, string containerId)
{
this._cosmosClient = Throw.IfNull(cosmosClient);
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosCheckpointStore));
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
this._ownsClient = false;
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Reflection;
using Microsoft.Azure.Cosmos;
namespace Microsoft.Agents.AI.CosmosNoSql;
/// <summary>
/// Provides shared Cosmos DB client configuration for Agent Framework Cosmos NoSQL integrations.
/// Ensures all internally-created <see cref="CosmosClient"/> instances carry a consistent
/// <see cref="CosmosClientOptions.ApplicationName"/> for telemetry and diagnostics.
/// </summary>
internal static class CosmosOptionsHelper
{
/// <summary>
/// Maximum length allowed by the Cosmos DB .NET SDK for <see cref="CosmosClientOptions.ApplicationName"/>.
/// </summary>
private const int MaxApplicationNameLength = 64;
private static readonly string s_version = GetVersion();
/// <summary>
/// Creates a <see cref="CosmosClientOptions"/> instance pre-configured with the
/// Agent Framework application name for User-Agent identification.
/// </summary>
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
/// <returns>A new <see cref="CosmosClientOptions"/> with <see cref="CosmosClientOptions.ApplicationName"/> set.</returns>
public static CosmosClientOptions CreateOptions(string component)
{
return new CosmosClientOptions
{
ApplicationName = BuildApplicationName(component)
};
}
/// <summary>
/// Ensures the given <see cref="CosmosClient"/> has an <see cref="CosmosClientOptions.ApplicationName"/> set.
/// If the client already has a non-empty ApplicationName, it is not overridden.
/// </summary>
/// <param name="cosmosClient">The client to apply the application name to.</param>
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
public static void EnsureApplicationName(CosmosClient cosmosClient, string component)
{
if (string.IsNullOrWhiteSpace(cosmosClient.ClientOptions.ApplicationName))
{
cosmosClient.ClientOptions.ApplicationName = BuildApplicationName(component);
}
}
private static string BuildApplicationName(string component)
{
var applicationName = $"Microsoft.Agents.AI.CosmosNoSql.{component}/{s_version}";
if (applicationName.Length > MaxApplicationNameLength)
{
applicationName = applicationName.Substring(0, MaxApplicationNameLength);
}
return applicationName;
}
private static string GetVersion()
{
if (typeof(CosmosOptionsHelper).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion is string version)
{
int pos = version.IndexOf('+', System.StringComparison.Ordinal);
if (pos >= 0)
{
version = version.Substring(0, pos);
}
if (version.Length > 0)
{
return version;
}
}
return "unknown";
}
}
@@ -0,0 +1,75 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.CosmosNoSql;
using Microsoft.Azure.Cosmos;
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
public sealed class CosmosOptionsHelperTests
{
[Fact]
public void CreateOptions_SetsApplicationName_WithComponentAndVersion()
{
// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
// Assert
Assert.NotNull(options.ApplicationName);
Assert.StartsWith("Microsoft.Agents.AI.CosmosNoSql.CosmosChatHistoryProvider/", options.ApplicationName);
}
[Fact]
public void CreateOptions_DifferentComponents_ProduceDifferentNames()
{
// Act
var chatOptions = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
var checkpointOptions = CosmosOptionsHelper.CreateOptions("CosmosCheckpointStore");
// Assert
Assert.NotEqual(chatOptions.ApplicationName, checkpointOptions.ApplicationName);
Assert.Contains("CosmosChatHistoryProvider", chatOptions.ApplicationName);
Assert.Contains("CosmosCheckpointStore", checkpointOptions.ApplicationName);
}
[Fact]
public void CreateOptions_ApplicationName_DoesNotExceedMaxLength()
{
// Use a deliberately long component name to trigger truncation
var longComponent = new string('X', 100);
// Act
var options = CosmosOptionsHelper.CreateOptions(longComponent);
// Assert
Assert.True(options.ApplicationName!.Length <= 64,
$"ApplicationName length {options.ApplicationName.Length} exceeds max 64");
}
[Fact]
public void EnsureApplicationName_SetsName_WhenClientHasNone()
{
// Arrange
var clientOptions = new CosmosClientOptions();
Assert.Null(clientOptions.ApplicationName);
// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
// Assert - verify the returned options have ApplicationName set
Assert.NotNull(options.ApplicationName);
Assert.NotEmpty(options.ApplicationName);
}
[Fact]
public void CreateOptions_ApplicationName_ContainsVersion()
{
// Act
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
// Assert - should contain a "/" followed by version info
Assert.Contains("/", options.ApplicationName);
var parts = options.ApplicationName!.Split('/');
Assert.Equal(2, parts.Length);
Assert.False(string.IsNullOrWhiteSpace(parts[1]), "Version portion should not be empty");
}
}