mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
5e830f4dc9
commit
0f483fa968
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user