Skip to content

Commit b6c7480

Browse files
Copilotaaronpowelldavidfowl
authored
Add support for configuring OpenTelemetryChatClient.EnableSensitiveData, MEAI telemetry sources, and expose DisableTracing property (#846)
* Initial plan * Add support for configuring OpenTelemetryChatClient.EnableSensitiveData and MEAI telemetry sources Co-authored-by: aaronpowell <[email protected]> * Update documentation for OpenTelemetry configuration support Co-authored-by: aaronpowell <[email protected]> * Expose DisableTracing property on OllamaSharpSettings publicly Co-authored-by: davidfowl <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: aaronpowell <[email protected]> Co-authored-by: davidfowl <[email protected]> Co-authored-by: Aaron Powell <[email protected]>
1 parent e79ebd8 commit b6c7480

File tree

5 files changed

+167
-6
lines changed

5 files changed

+167
-6
lines changed

src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.Extensions.DependencyInjection;
33
using Microsoft.Extensions.Logging;
44
using OllamaSharp;
5+
using OpenTelemetry;
56

67
namespace Microsoft.Extensions.Hosting;
78

@@ -10,6 +11,8 @@ namespace Microsoft.Extensions.Hosting;
1011
/// </summary>
1112
public static class AspireOllamaChatClientExtensions
1213
{
14+
private const string MeaiTelemetrySourceName = "Experimental.Microsoft.Extensions.AI";
15+
1316
/// <summary>
1417
/// Registers a singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
1518
/// </summary>
@@ -19,8 +22,25 @@ public static ChatClientBuilder AddChatClient(this AspireOllamaApiClientBuilder
1922
{
2023
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
2124

25+
return builder.AddChatClient(configureOpenTelemetry: null);
26+
}
27+
28+
/// <summary>
29+
/// Registers a singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
30+
/// </summary>
31+
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
32+
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
33+
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
34+
public static ChatClientBuilder AddChatClient(
35+
this AspireOllamaApiClientBuilder builder,
36+
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
37+
{
38+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
39+
40+
AddTelemetrySource(builder.HostBuilder);
41+
2242
return builder.HostBuilder.Services.AddChatClient(
23-
services => CreateInnerChatClient(services, builder));
43+
services => CreateInnerChatClient(services, builder, configureOpenTelemetry));
2444
}
2545

2646
/// <summary>
@@ -33,7 +53,22 @@ public static ChatClientBuilder AddKeyedChatClient(
3353
{
3454
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
3555

36-
return builder.AddKeyedChatClient(builder.ServiceKey);
56+
return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry: null);
57+
}
58+
59+
/// <summary>
60+
/// Registers a keyed singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/>.
61+
/// </summary>
62+
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
63+
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
64+
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
65+
public static ChatClientBuilder AddKeyedChatClient(
66+
this AspireOllamaApiClientBuilder builder,
67+
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
68+
{
69+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
70+
71+
return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry);
3772
}
3873

3974
/// <summary>
@@ -49,9 +84,29 @@ public static ChatClientBuilder AddKeyedChatClient(
4984
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
5085
ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey));
5186

87+
return builder.AddKeyedChatClient(serviceKey, configureOpenTelemetry: null);
88+
}
89+
90+
/// <summary>
91+
/// Registers a keyed singleton <see cref="IChatClient"/> in the services provided by the <paramref name="builder"/> using the specified service key.
92+
/// </summary>
93+
/// <param name="builder">An <see cref="AspireOllamaApiClientBuilder" />.</param>
94+
/// <param name="serviceKey">The service key to use for registering the <see cref="IChatClient"/>.</param>
95+
/// <param name="configureOpenTelemetry">An optional delegate that can be used for customizing the OpenTelemetry chat client.</param>
96+
/// <returns>A <see cref="ChatClientBuilder"/> that can be used to build a pipeline around the inner <see cref="IChatClient"/>.</returns>
97+
public static ChatClientBuilder AddKeyedChatClient(
98+
this AspireOllamaApiClientBuilder builder,
99+
object serviceKey,
100+
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
101+
{
102+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
103+
ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey));
104+
105+
AddTelemetrySource(builder.HostBuilder);
106+
52107
return builder.HostBuilder.Services.AddKeyedChatClient(
53108
serviceKey,
54-
services => CreateInnerChatClient(services, builder));
109+
services => CreateInnerChatClient(services, builder, configureOpenTelemetry));
55110
}
56111

57112
/// <summary>
@@ -61,7 +116,8 @@ public static ChatClientBuilder AddKeyedChatClient(
61116
/// </summary>
62117
private static IChatClient CreateInnerChatClient(
63118
IServiceProvider services,
64-
AspireOllamaApiClientBuilder builder)
119+
AspireOllamaApiClientBuilder builder,
120+
Action<OpenTelemetryChatClient>? configureOpenTelemetry)
65121
{
66122
var ollamaApiClient = services.GetRequiredKeyedService<IOllamaApiClient>(builder.ServiceKey);
67123

@@ -73,6 +129,22 @@ private static IChatClient CreateInnerChatClient(
73129
}
74130

75131
var loggerFactory = services.GetService<ILoggerFactory>();
76-
return new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)));
132+
var otelChatClient = new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)), MeaiTelemetrySourceName);
133+
134+
configureOpenTelemetry?.Invoke(otelChatClient);
135+
136+
return otelChatClient;
137+
}
138+
139+
/// <summary>
140+
/// Add the MEAI telemetry source to OpenTelemetry tracing.
141+
/// </summary>
142+
private static void AddTelemetrySource(IHostApplicationBuilder hostBuilder)
143+
{
144+
hostBuilder.Services.AddOpenTelemetry()
145+
.WithTracing(tracing =>
146+
{
147+
tracing.AddSource(MeaiTelemetrySourceName);
148+
});
77149
}
78150
}

src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="Microsoft.Extensions.Http" />
1313
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
1414
<PackageReference Include="OllamaSharp" />
15+
<PackageReference Include="OpenTelemetry.Extensions.Hosting" />
1516
</ItemGroup>
1617

1718
<ItemGroup>

src/CommunityToolkit.Aspire.OllamaSharp/OllamaSharpSettings.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ public sealed class OllamaSharpSettings
3737
/// Gets or sets a boolean value that indicates whether tracing is disabled or not.
3838
/// </summary>
3939
/// <remarks>Currently, the OllamaSharp SDK does not support tracing, but this is here for future use.</remarks>
40-
internal bool DisableTracing { get; set; }
40+
public bool DisableTracing { get; set; }
4141

4242
}

src/CommunityToolkit.Aspire.OllamaSharp/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ public class MyService(IOllamaApiClient ollamaApiClient)
3737

3838
To use the integration with Microsoft.Extensions.AI, call the `AddOllamaSharpChatClient` or `AddOllamaSharpEmbeddingGenerator` extension method in the _Program.cs_ file of your project. These methods take the connection name as a parameter, just as `AddOllamaApiClient` does, and will register the `IOllamaApiClient`, as well as the `IChatClient` or `IEmbeddingGenerator` in the DI container. The `IEmbeddingsGenerator` is registered with the generic arguments of `<string, Embedding<float>>`.
3939

40+
#### Configuring OpenTelemetry
41+
42+
When using the chat client integration, you can optionally configure the OpenTelemetry chat client to control telemetry behavior such as enabling sensitive data:
43+
44+
```csharp
45+
builder.AddOllamaApiClient("ollama")
46+
.AddChatClient(otel => otel.EnableSensitiveData = true);
47+
```
48+
49+
The integration automatically registers the Microsoft.Extensions.AI telemetry source (`Experimental.Microsoft.Extensions.AI`) with OpenTelemetry for distributed tracing.
50+
4051
## Additional documentation
4152

4253
- https://github.com/awaescher/OllamaSharp

tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,83 @@ public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys()
219219
Assert.Equal(chatClient1 as IOllamaApiClient, embeddingGenerator as IOllamaApiClient);
220220
}
221221

222+
[Theory]
223+
[InlineData(true)]
224+
[InlineData(false)]
225+
public void CanConfigureOpenTelemetrySensitiveData(bool useKeyed)
226+
{
227+
var builder = Host.CreateEmptyApplicationBuilder(null);
228+
builder.Configuration.AddInMemoryCollection([
229+
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
230+
]);
231+
232+
if (useKeyed)
233+
{
234+
builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(otel => otel.EnableSensitiveData = true);
235+
}
236+
else
237+
{
238+
builder.AddOllamaApiClient("Ollama").AddChatClient(otel => otel.EnableSensitiveData = true);
239+
}
240+
241+
using var host = builder.Build();
242+
var client = useKeyed ?
243+
host.Services.GetRequiredKeyedService<IChatClient>("Ollama") :
244+
host.Services.GetRequiredService<IChatClient>();
245+
246+
// Navigate through the client chain to find the OpenTelemetryChatClient
247+
var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
248+
Assert.True(otelClient.EnableSensitiveData);
249+
}
250+
251+
[Fact]
252+
public void CanConfigureOpenTelemetrySensitiveDataWithCustomServiceKey()
253+
{
254+
var builder = Host.CreateEmptyApplicationBuilder(null);
255+
builder.Configuration.AddInMemoryCollection([
256+
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
257+
]);
258+
259+
builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama")
260+
.AddKeyedChatClient("ChatKey", otel => otel.EnableSensitiveData = true);
261+
262+
using var host = builder.Build();
263+
var client = host.Services.GetRequiredKeyedService<IChatClient>("ChatKey");
264+
265+
var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
266+
Assert.True(otelClient.EnableSensitiveData);
267+
}
268+
269+
[Theory]
270+
[InlineData(true)]
271+
[InlineData(false)]
272+
public void OpenTelemetryConfigurationIsOptional(bool useKeyed)
273+
{
274+
var builder = Host.CreateEmptyApplicationBuilder(null);
275+
builder.Configuration.AddInMemoryCollection([
276+
new KeyValuePair<string, string?>("ConnectionStrings:Ollama", $"Endpoint={Endpoint}")
277+
]);
278+
279+
// Test that we can still call the methods without configuration
280+
if (useKeyed)
281+
{
282+
builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(configureOpenTelemetry: null);
283+
}
284+
else
285+
{
286+
builder.AddOllamaApiClient("Ollama").AddChatClient(configureOpenTelemetry: null);
287+
}
288+
289+
using var host = builder.Build();
290+
var client = useKeyed ?
291+
host.Services.GetRequiredKeyedService<IChatClient>("Ollama") :
292+
host.Services.GetRequiredService<IChatClient>();
293+
294+
var otelClient = Assert.IsType<OpenTelemetryChatClient>(client);
295+
// EnableSensitiveData should be false by default
296+
Assert.False(otelClient.EnableSensitiveData);
297+
}
298+
222299
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")]
223300
private static extern IChatClient GetInnerClient(DelegatingChatClient client);
224301
}

0 commit comments

Comments
 (0)