diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs index adccd1686..128e70713 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/AspireOllamaChatClientExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using OllamaSharp; +using OpenTelemetry; namespace Microsoft.Extensions.Hosting; @@ -10,6 +11,8 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireOllamaChatClientExtensions { + private const string MeaiTelemetrySourceName = "Experimental.Microsoft.Extensions.AI"; + /// /// Registers a singleton in the services provided by the . /// @@ -19,8 +22,25 @@ public static ChatClientBuilder AddChatClient(this AspireOllamaApiClientBuilder { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + return builder.AddChatClient(configureOpenTelemetry: null); + } + + /// + /// Registers a singleton in the services provided by the . + /// + /// An . + /// An optional delegate that can be used for customizing the OpenTelemetry chat client. + /// A that can be used to build a pipeline around the inner . + public static ChatClientBuilder AddChatClient( + this AspireOllamaApiClientBuilder builder, + Action? configureOpenTelemetry) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + AddTelemetrySource(builder.HostBuilder); + return builder.HostBuilder.Services.AddChatClient( - services => CreateInnerChatClient(services, builder)); + services => CreateInnerChatClient(services, builder, configureOpenTelemetry)); } /// @@ -33,7 +53,22 @@ public static ChatClientBuilder AddKeyedChatClient( { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - return builder.AddKeyedChatClient(builder.ServiceKey); + return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry: null); + } + + /// + /// Registers a keyed singleton in the services provided by the . + /// + /// An . + /// An optional delegate that can be used for customizing the OpenTelemetry chat client. + /// A that can be used to build a pipeline around the inner . + public static ChatClientBuilder AddKeyedChatClient( + this AspireOllamaApiClientBuilder builder, + Action? configureOpenTelemetry) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + return builder.AddKeyedChatClient(builder.ServiceKey, configureOpenTelemetry); } /// @@ -49,9 +84,29 @@ public static ChatClientBuilder AddKeyedChatClient( ArgumentNullException.ThrowIfNull(builder, nameof(builder)); ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + return builder.AddKeyedChatClient(serviceKey, configureOpenTelemetry: null); + } + + /// + /// Registers a keyed singleton in the services provided by the using the specified service key. + /// + /// An . + /// The service key to use for registering the . + /// An optional delegate that can be used for customizing the OpenTelemetry chat client. + /// A that can be used to build a pipeline around the inner . + public static ChatClientBuilder AddKeyedChatClient( + this AspireOllamaApiClientBuilder builder, + object serviceKey, + Action? configureOpenTelemetry) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(serviceKey, nameof(serviceKey)); + + AddTelemetrySource(builder.HostBuilder); + return builder.HostBuilder.Services.AddKeyedChatClient( serviceKey, - services => CreateInnerChatClient(services, builder)); + services => CreateInnerChatClient(services, builder, configureOpenTelemetry)); } /// @@ -61,7 +116,8 @@ public static ChatClientBuilder AddKeyedChatClient( /// private static IChatClient CreateInnerChatClient( IServiceProvider services, - AspireOllamaApiClientBuilder builder) + AspireOllamaApiClientBuilder builder, + Action? configureOpenTelemetry) { var ollamaApiClient = services.GetRequiredKeyedService(builder.ServiceKey); @@ -73,6 +129,22 @@ private static IChatClient CreateInnerChatClient( } var loggerFactory = services.GetService(); - return new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient))); + var otelChatClient = new OpenTelemetryChatClient(result, loggerFactory?.CreateLogger(typeof(OpenTelemetryChatClient)), MeaiTelemetrySourceName); + + configureOpenTelemetry?.Invoke(otelChatClient); + + return otelChatClient; + } + + /// + /// Add the MEAI telemetry source to OpenTelemetry tracing. + /// + private static void AddTelemetrySource(IHostApplicationBuilder hostBuilder) + { + hostBuilder.Services.AddOpenTelemetry() + .WithTracing(tracing => + { + tracing.AddSource(MeaiTelemetrySourceName); + }); } } diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj b/src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj index 2170bccbb..02b64ccbd 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj +++ b/src/CommunityToolkit.Aspire.OllamaSharp/CommunityToolkit.Aspire.OllamaSharp.csproj @@ -12,6 +12,7 @@ + diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/OllamaSharpSettings.cs b/src/CommunityToolkit.Aspire.OllamaSharp/OllamaSharpSettings.cs index c8bd59083..13b116ba6 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/OllamaSharpSettings.cs +++ b/src/CommunityToolkit.Aspire.OllamaSharp/OllamaSharpSettings.cs @@ -37,6 +37,6 @@ public sealed class OllamaSharpSettings /// Gets or sets a boolean value that indicates whether tracing is disabled or not. /// /// Currently, the OllamaSharp SDK does not support tracing, but this is here for future use. - internal bool DisableTracing { get; set; } + public bool DisableTracing { get; set; } } diff --git a/src/CommunityToolkit.Aspire.OllamaSharp/README.md b/src/CommunityToolkit.Aspire.OllamaSharp/README.md index c08469fdd..4d51e4188 100644 --- a/src/CommunityToolkit.Aspire.OllamaSharp/README.md +++ b/src/CommunityToolkit.Aspire.OllamaSharp/README.md @@ -37,6 +37,17 @@ public class MyService(IOllamaApiClient ollamaApiClient) 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 `>`. +#### Configuring OpenTelemetry + +When using the chat client integration, you can optionally configure the OpenTelemetry chat client to control telemetry behavior such as enabling sensitive data: + +```csharp +builder.AddOllamaApiClient("ollama") + .AddChatClient(otel => otel.EnableSensitiveData = true); +``` + +The integration automatically registers the Microsoft.Extensions.AI telemetry source (`Experimental.Microsoft.Extensions.AI`) with OpenTelemetry for distributed tracing. + ## Additional documentation - https://github.com/awaescher/OllamaSharp diff --git a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs index b409e9072..1dfd4f2fb 100644 --- a/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs +++ b/tests/CommunityToolkit.Aspire.OllamaSharp.Tests/OllamaSharpIChatClientTests.cs @@ -219,6 +219,83 @@ public void CanMixChatClientsAndEmbeddingGeneratorsWithCustomServiceKeys() Assert.Equal(chatClient1 as IOllamaApiClient, embeddingGenerator as IOllamaApiClient); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CanConfigureOpenTelemetrySensitiveData(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + if (useKeyed) + { + builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(otel => otel.EnableSensitiveData = true); + } + else + { + builder.AddOllamaApiClient("Ollama").AddChatClient(otel => otel.EnableSensitiveData = true); + } + + using var host = builder.Build(); + var client = useKeyed ? + host.Services.GetRequiredKeyedService("Ollama") : + host.Services.GetRequiredService(); + + // Navigate through the client chain to find the OpenTelemetryChatClient + var otelClient = Assert.IsType(client); + Assert.True(otelClient.EnableSensitiveData); + } + + [Fact] + public void CanConfigureOpenTelemetrySensitiveDataWithCustomServiceKey() + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + builder.AddKeyedOllamaApiClient("OllamaKey", "Ollama") + .AddKeyedChatClient("ChatKey", otel => otel.EnableSensitiveData = true); + + using var host = builder.Build(); + var client = host.Services.GetRequiredKeyedService("ChatKey"); + + var otelClient = Assert.IsType(client); + Assert.True(otelClient.EnableSensitiveData); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void OpenTelemetryConfigurationIsOptional(bool useKeyed) + { + var builder = Host.CreateEmptyApplicationBuilder(null); + builder.Configuration.AddInMemoryCollection([ + new KeyValuePair("ConnectionStrings:Ollama", $"Endpoint={Endpoint}") + ]); + + // Test that we can still call the methods without configuration + if (useKeyed) + { + builder.AddKeyedOllamaApiClient("Ollama").AddKeyedChatClient(configureOpenTelemetry: null); + } + else + { + builder.AddOllamaApiClient("Ollama").AddChatClient(configureOpenTelemetry: null); + } + + using var host = builder.Build(); + var client = useKeyed ? + host.Services.GetRequiredKeyedService("Ollama") : + host.Services.GetRequiredService(); + + var otelClient = Assert.IsType(client); + // EnableSensitiveData should be false by default + Assert.False(otelClient.EnableSensitiveData); + } + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_InnerClient")] private static extern IChatClient GetInnerClient(DelegatingChatClient client); }