Skip to content

Commit 0f483fa

Browse files
authored
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>
1 parent 5e830f4 commit 0f483fa

4 files changed

Lines changed: 165 additions & 8 deletions

File tree

dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosChatHistoryProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Azure.Core;
11+
using Microsoft.Agents.AI.CosmosNoSql;
1112
using Microsoft.Azure.Cosmos;
1213
using Microsoft.Extensions.AI;
1314
using Microsoft.Shared.Diagnostics;
@@ -126,6 +127,7 @@ public CosmosChatHistoryProvider(
126127
Throw.IfNull(stateInitializer),
127128
stateKey ?? this.GetType().Name);
128129
this._cosmosClient = Throw.IfNull(cosmosClient);
130+
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosChatHistoryProvider));
129131
this.DatabaseId = Throw.IfNullOrWhitespace(databaseId);
130132
this.ContainerId = Throw.IfNullOrWhitespace(containerId);
131133
this._container = this._cosmosClient.GetContainer(databaseId, containerId);
@@ -157,7 +159,7 @@ public CosmosChatHistoryProvider(
157159
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
158160
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
159161
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
160-
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
162+
: this(new CosmosClient(Throw.IfNullOrWhitespace(connectionString), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
161163
{
162164
}
163165

@@ -185,7 +187,7 @@ public CosmosChatHistoryProvider(
185187
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? provideOutputMessageFilter = null,
186188
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputRequestMessageFilter = null,
187189
Func<IEnumerable<ChatMessage>, IEnumerable<ChatMessage>>? storeInputResponseMessageFilter = null)
188-
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential)), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
190+
: this(new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), CosmosOptionsHelper.CreateOptions(nameof(CosmosChatHistoryProvider))), databaseId, containerId, stateInitializer, ownsClient: true, stateKey, provideOutputMessageFilter, storeInputRequestMessageFilter, storeInputResponseMessageFilter)
189191
{
190192
}
191193

dotnet/src/Microsoft.Agents.AI.CosmosNoSql/CosmosCheckpointStore.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Text.Json;
88
using System.Threading.Tasks;
99
using Azure.Core;
10+
using Microsoft.Agents.AI.CosmosNoSql;
1011
using Microsoft.Azure.Cosmos;
1112
using Microsoft.Shared.Diagnostics;
1213
using Newtonsoft.Json;
@@ -37,7 +38,7 @@ public class CosmosCheckpointStore<T> : JsonCheckpointStore, IDisposable
3738
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
3839
public CosmosCheckpointStore(string connectionString, string databaseId, string containerId)
3940
{
40-
var cosmosClientOptions = new CosmosClientOptions();
41+
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));
4142

4243
this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(connectionString), cosmosClientOptions);
4344
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
@@ -55,12 +56,10 @@ public CosmosCheckpointStore(string connectionString, string databaseId, string
5556
/// <exception cref="ArgumentException">Thrown when any string parameter is null or whitespace.</exception>
5657
public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCredential, string databaseId, string containerId)
5758
{
58-
var cosmosClientOptions = new CosmosClientOptions
59+
var cosmosClientOptions = CosmosOptionsHelper.CreateOptions(nameof(CosmosCheckpointStore));
60+
cosmosClientOptions.SerializerOptions = new CosmosSerializationOptions
5961
{
60-
SerializerOptions = new CosmosSerializationOptions
61-
{
62-
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
63-
}
62+
PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
6463
};
6564

6665
this._cosmosClient = new CosmosClient(Throw.IfNullOrWhitespace(accountEndpoint), Throw.IfNull(tokenCredential), cosmosClientOptions);
@@ -79,6 +78,7 @@ public CosmosCheckpointStore(string accountEndpoint, TokenCredential tokenCreden
7978
public CosmosCheckpointStore(CosmosClient cosmosClient, string databaseId, string containerId)
8079
{
8180
this._cosmosClient = Throw.IfNull(cosmosClient);
81+
CosmosOptionsHelper.EnsureApplicationName(this._cosmosClient, nameof(CosmosCheckpointStore));
8282

8383
this._container = this._cosmosClient.GetContainer(Throw.IfNullOrWhitespace(databaseId), Throw.IfNullOrWhitespace(containerId));
8484
this._ownsClient = false;
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.Reflection;
4+
using Microsoft.Azure.Cosmos;
5+
6+
namespace Microsoft.Agents.AI.CosmosNoSql;
7+
8+
/// <summary>
9+
/// Provides shared Cosmos DB client configuration for Agent Framework Cosmos NoSQL integrations.
10+
/// Ensures all internally-created <see cref="CosmosClient"/> instances carry a consistent
11+
/// <see cref="CosmosClientOptions.ApplicationName"/> for telemetry and diagnostics.
12+
/// </summary>
13+
internal static class CosmosOptionsHelper
14+
{
15+
/// <summary>
16+
/// Maximum length allowed by the Cosmos DB .NET SDK for <see cref="CosmosClientOptions.ApplicationName"/>.
17+
/// </summary>
18+
private const int MaxApplicationNameLength = 64;
19+
20+
private static readonly string s_version = GetVersion();
21+
22+
/// <summary>
23+
/// Creates a <see cref="CosmosClientOptions"/> instance pre-configured with the
24+
/// Agent Framework application name for User-Agent identification.
25+
/// </summary>
26+
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
27+
/// <returns>A new <see cref="CosmosClientOptions"/> with <see cref="CosmosClientOptions.ApplicationName"/> set.</returns>
28+
public static CosmosClientOptions CreateOptions(string component)
29+
{
30+
return new CosmosClientOptions
31+
{
32+
ApplicationName = BuildApplicationName(component)
33+
};
34+
}
35+
36+
/// <summary>
37+
/// Ensures the given <see cref="CosmosClient"/> has an <see cref="CosmosClientOptions.ApplicationName"/> set.
38+
/// If the client already has a non-empty ApplicationName, it is not overridden.
39+
/// </summary>
40+
/// <param name="cosmosClient">The client to apply the application name to.</param>
41+
/// <param name="component">The fully-qualified component class name (e.g. "CosmosChatHistoryProvider").</param>
42+
public static void EnsureApplicationName(CosmosClient cosmosClient, string component)
43+
{
44+
if (string.IsNullOrWhiteSpace(cosmosClient.ClientOptions.ApplicationName))
45+
{
46+
cosmosClient.ClientOptions.ApplicationName = BuildApplicationName(component);
47+
}
48+
}
49+
50+
private static string BuildApplicationName(string component)
51+
{
52+
var applicationName = $"Microsoft.Agents.AI.CosmosNoSql.{component}/{s_version}";
53+
54+
if (applicationName.Length > MaxApplicationNameLength)
55+
{
56+
applicationName = applicationName.Substring(0, MaxApplicationNameLength);
57+
}
58+
59+
return applicationName;
60+
}
61+
62+
private static string GetVersion()
63+
{
64+
if (typeof(CosmosOptionsHelper).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion is string version)
65+
{
66+
int pos = version.IndexOf('+', System.StringComparison.Ordinal);
67+
if (pos >= 0)
68+
{
69+
version = version.Substring(0, pos);
70+
}
71+
72+
if (version.Length > 0)
73+
{
74+
return version;
75+
}
76+
}
77+
78+
return "unknown";
79+
}
80+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.Agents.AI.CosmosNoSql;
4+
using Microsoft.Azure.Cosmos;
5+
6+
namespace Microsoft.Agents.AI.CosmosNoSql.UnitTests;
7+
8+
public sealed class CosmosOptionsHelperTests
9+
{
10+
[Fact]
11+
public void CreateOptions_SetsApplicationName_WithComponentAndVersion()
12+
{
13+
// Act
14+
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
15+
16+
// Assert
17+
Assert.NotNull(options.ApplicationName);
18+
Assert.StartsWith("Microsoft.Agents.AI.CosmosNoSql.CosmosChatHistoryProvider/", options.ApplicationName);
19+
}
20+
21+
[Fact]
22+
public void CreateOptions_DifferentComponents_ProduceDifferentNames()
23+
{
24+
// Act
25+
var chatOptions = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
26+
var checkpointOptions = CosmosOptionsHelper.CreateOptions("CosmosCheckpointStore");
27+
28+
// Assert
29+
Assert.NotEqual(chatOptions.ApplicationName, checkpointOptions.ApplicationName);
30+
Assert.Contains("CosmosChatHistoryProvider", chatOptions.ApplicationName);
31+
Assert.Contains("CosmosCheckpointStore", checkpointOptions.ApplicationName);
32+
}
33+
34+
[Fact]
35+
public void CreateOptions_ApplicationName_DoesNotExceedMaxLength()
36+
{
37+
// Use a deliberately long component name to trigger truncation
38+
var longComponent = new string('X', 100);
39+
40+
// Act
41+
var options = CosmosOptionsHelper.CreateOptions(longComponent);
42+
43+
// Assert
44+
Assert.True(options.ApplicationName!.Length <= 64,
45+
$"ApplicationName length {options.ApplicationName.Length} exceeds max 64");
46+
}
47+
48+
[Fact]
49+
public void EnsureApplicationName_SetsName_WhenClientHasNone()
50+
{
51+
// Arrange
52+
var clientOptions = new CosmosClientOptions();
53+
Assert.Null(clientOptions.ApplicationName);
54+
55+
// Act
56+
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
57+
58+
// Assert - verify the returned options have ApplicationName set
59+
Assert.NotNull(options.ApplicationName);
60+
Assert.NotEmpty(options.ApplicationName);
61+
}
62+
63+
[Fact]
64+
public void CreateOptions_ApplicationName_ContainsVersion()
65+
{
66+
// Act
67+
var options = CosmosOptionsHelper.CreateOptions("CosmosChatHistoryProvider");
68+
69+
// Assert - should contain a "/" followed by version info
70+
Assert.Contains("/", options.ApplicationName);
71+
var parts = options.ApplicationName!.Split('/');
72+
Assert.Equal(2, parts.Length);
73+
Assert.False(string.IsNullOrWhiteSpace(parts[1]), "Version portion should not be empty");
74+
}
75+
}

0 commit comments

Comments
 (0)