Skip to content

.Net: [MEVD] Added OpenTelemetry decorators for vector store abstractions #11207

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
762adb7
Added metadata and GetService for IVectorStore
dmytrostruk Mar 18, 2025
a718acd
Added metadata and GetService for IVectorStoreRecordCollection
dmytrostruk Mar 18, 2025
b5bc68c
Small updates in tests
dmytrostruk Mar 19, 2025
c35ac3e
Updated baseline version
dmytrostruk Mar 19, 2025
5844a65
Small updates
dmytrostruk Mar 19, 2025
211f149
Merge branch 'main' into vector-data-metadata
dmytrostruk Mar 19, 2025
cdbeeb5
Updated suppression files
dmytrostruk Mar 19, 2025
dc1cba2
Fixed Azure AI Search unit tests
dmytrostruk Mar 19, 2025
f79ee7b
Fixed unit tests for Sqlite
dmytrostruk Mar 19, 2025
648278f
Fixed more unit tests
dmytrostruk Mar 19, 2025
ba81b70
.Net MEVD: Split batches to account for the SQL Server 2100 parameter…
adamsitnik Mar 19, 2025
f631bb2
Merge branch 'main' into vector-data-metadata
dmytrostruk Mar 19, 2025
2cade4f
Fixed SQL Server tests
dmytrostruk Mar 19, 2025
f85c217
.Net: Fixed links (#11068)
dmytrostruk Mar 19, 2025
5ab0890
Merge branch 'main' into vector-data-metadata
dmytrostruk Mar 19, 2025
dd20b69
Addressed PR feedback
dmytrostruk Mar 20, 2025
8791b69
Merge branch 'vector-data-metadata' of https://github.com/dmytrostruk…
dmytrostruk Mar 20, 2025
42bad91
Merge branch 'feature-vector-data-preb2' into vector-data-metadata
dmytrostruk Mar 20, 2025
8ea1c84
More updates after merge
dmytrostruk Mar 20, 2025
df076b1
Small fix
dmytrostruk Mar 20, 2025
fd907a4
Removed some of the metadata classes
dmytrostruk Mar 20, 2025
21dcf2a
Added OpenTelemetry decorators
dmytrostruk Mar 25, 2025
bcf5483
Added unit tests
dmytrostruk Mar 25, 2025
27dd0c2
Added tracing and metering examples
dmytrostruk Mar 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dotnet/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<PackageVersion Include="OllamaSharp" Version="5.1.7" />
<PackageVersion Include="OpenAI" Version="[2.2.0-beta.4]" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.InMemory" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.9.0" />
Expand Down
2 changes: 2 additions & 0 deletions dotnet/samples/Concepts/Concepts.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.Exporter" />
<PackageReference Include="Microsoft.Net.Compilers.Toolset" />
<PackageReference Include="Docker.DotNet" />
<PackageReference Include="Google.Apis.Auth" />
Expand All @@ -22,6 +23,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Npgsql" />
<PackageReference Include="OpenAI" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="xRetry" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.abstractions" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ public IVectorStoreRecordCollection<TKey, TRecord> GetCollection<TKey, TRecord>(
return embeddingStore;
}

/// <inheritdoc />
public object? GetService(Type serviceType, object? serviceKey = null)
{
ArgumentNullException.ThrowIfNull(serviceType);

return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
this._decoratedVectorStore.GetService(serviceType, serviceKey);
}

/// <inheritdoc />
public IAsyncEnumerable<string> ListCollectionNamesAsync(CancellationToken cancellationToken = default)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public TextEmbeddingVectorStoreRecordCollection(IVectorStoreRecordCollection<TKe
}

/// <inheritdoc />
[Obsolete("Use GetService(typeof(VectorStoreRecordCollectionMetadata)) to get an information about vector store record collection.")]
public string CollectionName => this._decoratedVectorStoreRecordCollection.CollectionName;

/// <inheritdoc />
Expand Down Expand Up @@ -132,6 +133,16 @@ public async Task<VectorSearchResults<TRecord>> VectorizableTextSearchAsync(stri
return await this.VectorizedSearchAsync(embeddingValue, options, cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc />
public object? GetService(Type serviceType, object? serviceKey = null)
{
ArgumentNullException.ThrowIfNull(serviceType);

return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
this._decoratedVectorStoreRecordCollection.GetService(serviceType, serviceKey);
}

/// <summary>
/// Generate and add embeddings for each embedding field that has a <see cref="GenerateTextEmbeddingAttribute"/> on the provided record.
/// </summary>
Expand Down
151 changes: 150 additions & 1 deletion dotnet/samples/Concepts/Memory/VectorStore_Telemetry.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.

using System.Diagnostics;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.VectorData;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;
using Microsoft.SemanticKernel.Connectors.InMemory;
using Microsoft.SemanticKernel.Embeddings;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

namespace Memory;

/// <summary>
/// A simple example showing how to ingest data into a vector store and then use vector search to find related records to a given string
/// with enabled telemetry.
/// </summary>
public class VectorStore_Telemetry(ITestOutputHelper output) : BaseTest(output)
public class VectorStore_Telemetry(ITestOutputHelper output) : BaseTest(output, redirectSystemConsoleOutput: true)
{
/// <summary>
/// Instance of <see cref="ActivitySource"/> for the example's main activity.
/// </summary>
private static readonly ActivitySource s_activitySource = new("VectorStoreTelemetry.Example");

[Fact]
public async Task LoggingManualRegistrationAsync()
{
Expand Down Expand Up @@ -91,6 +101,143 @@ public async Task LoggingDependencyInjectionAsync()
// Result: Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data.
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task TracingAsync(bool useApplicationInsights)
{
// Create an embedding generation service.
var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
new AzureCliCredential());

// Manually construct an InMemory vector store with enabled OpenTelemetry.
var vectorStore = new InMemoryVectorStore()
.AsBuilder()
.UseOpenTelemetry()
.Build();

using var tracerProvider = GetTracerProvider(useApplicationInsights);

using var activity = s_activitySource.StartActivity("MainActivity");
Console.WriteLine($"Operation/Trace ID: {Activity.Current?.TraceId}");

await RunExampleAsync(textEmbeddingGenerationService, vectorStore);

// Output:
// Activity.DisplayName: create_collection_if_not_exists skglossary
// Activity.Duration: 00:00:00.0026442
// Activity.Tags:
// db.operation.name: create_collection_if_not_exists
// db.collection.name: skglossary
// db.system.name: inmemory
// Activity.DisplayName: upsert skglossary
// Activity.Duration: 00:00:00.0010686
// Activity.Tags:
// db.operation.name: upsert
// db.collection.name: skglossary
// db.system.name: inmemory
// and more...

// Search string: What is an Application Programming Interface
// Result: Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data.
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task MeteringAsync(bool useApplicationInsights)
{
// Create an embedding generation service.
var textEmbeddingGenerationService = new AzureOpenAITextEmbeddingGenerationService(
TestConfiguration.AzureOpenAIEmbeddings.DeploymentName,
TestConfiguration.AzureOpenAIEmbeddings.Endpoint,
new AzureCliCredential());

// Manually construct an InMemory vector store with enabled OpenTelemetry.
var vectorStore = new InMemoryVectorStore()
.AsBuilder()
.UseOpenTelemetry()
.Build();

using var meterProvider = GetMeterProvider(useApplicationInsights);

await RunExampleAsync(textEmbeddingGenerationService, vectorStore);

// Output:
// Metric Name: db.client.operation.duration, Duration of database client operations., Unit: s, Meter: Experimental.Microsoft.Extensions.VectorData

// (2025-03-25T20:03:17.9938116Z, 2025-03-25T20:03:20.2214978Z] db.collection.name: skglossary db.operation.name: create_collection_if_not_exists db.system.name: inmemory Histogram
// Value: Sum: 0.0015761 Count: 1 Min: 0.0015761 Max: 0.0015761

// (2025-03-25T20:03:17.9938116Z, 2025-03-25T20:03:20.2214978Z] db.collection.name: skglossary db.operation.name: upsert db.system.name: inmemory Histogram
// Value: Sum: 0.0011708 Count: 3 Min: 7E-06 Max: 0.0009944

// (2025-03-25T20:03:17.9938116Z, 2025-03-25T20:03:20.2214978Z] db.collection.name: skglossary db.operation.name: vectorized_search db.system.name: inmemory Histogram
// Value: Sum: 0.0109487 Count: 1 Min: 0.0109487 Max: 0.0109487

// Search string: What is an Application Programming Interface
// Result: Application Programming Interface. A set of rules and specifications that allow software components to communicate and exchange data.
}

#region private

private TracerProvider? GetTracerProvider(bool useApplicationInsights)
{
var tracerProviderBuilder = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Vector Data Tracing Example"))
.AddSource("Experimental.Microsoft.Extensions.VectorData*")
.AddSource(s_activitySource.Name);

if (useApplicationInsights)
{
var connectionString = TestConfiguration.ApplicationInsights.ConnectionString;

if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ConfigurationNotFoundException(
nameof(TestConfiguration.ApplicationInsights),
nameof(TestConfiguration.ApplicationInsights.ConnectionString));
}

tracerProviderBuilder.AddAzureMonitorTraceExporter(o => o.ConnectionString = connectionString);
}
else
{
tracerProviderBuilder.AddConsoleExporter();
}

return tracerProviderBuilder.Build();
}

private MeterProvider? GetMeterProvider(bool useApplicationInsights)
{
var meterProviderBuilder = OpenTelemetry.Sdk.CreateMeterProviderBuilder()
.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Vector Data Metering Example"))
.AddMeter("Experimental.Microsoft.Extensions.VectorData*");

if (useApplicationInsights)
{
var connectionString = TestConfiguration.ApplicationInsights.ConnectionString;

if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ConfigurationNotFoundException(
nameof(TestConfiguration.ApplicationInsights),
nameof(TestConfiguration.ApplicationInsights.ConnectionString));
}

meterProviderBuilder.AddAzureMonitorMetricExporter(o => o.ConnectionString = connectionString);
}
else
{
meterProviderBuilder.AddConsoleExporter();
}

return meterProviderBuilder.Build();
}

private async Task RunExampleAsync(
ITextEmbeddingGenerationService textEmbeddingGenerationService,
IVectorStore vectorStore)
Expand Down Expand Up @@ -177,4 +324,6 @@ private static IEnumerable<Glossary> CreateGlossaryEntries()
Definition = "Retrieval Augmented Generation - a term that refers to the process of retrieving additional data to provide as context to an LLM to use when generating a response (completion) to a user’s question (prompt)."
};
}

#endregion
}
10 changes: 10 additions & 0 deletions dotnet/samples/Concepts/Search/VectorStore_TextSearch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ public async Task<VectorSearchResults<TRecord>> VectorizableTextSearchAsync(stri

return await vectorizedSearch.VectorizedSearchAsync(vectorizedQuery, options, cancellationToken);
}

/// <inheritdoc />
public object? GetService(Type serviceType, object? serviceKey = null)
{
ArgumentNullException.ThrowIfNull(serviceType);

return
serviceKey is null && serviceType.IsInstanceOfType(this) ? this :
vectorizedSearch.GetService(serviceType, serviceKey);
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public AzureAISearchVectorStoreRecordCollectionTests()
this._searchClientMock = new Mock<SearchClient>(MockBehavior.Strict);
this._searchIndexClientMock = new Mock<SearchIndexClient>(MockBehavior.Strict);
this._searchIndexClientMock.Setup(x => x.GetSearchClient(TestCollectionName)).Returns(this._searchClientMock.Object);
this._searchIndexClientMock.Setup(x => x.ServiceName).Returns("TestService");
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public AzureAISearchVectorStoreTests()
this._searchClientMock = new Mock<SearchClient>(MockBehavior.Strict);
this._searchIndexClientMock = new Mock<SearchIndexClient>(MockBehavior.Strict);
this._searchIndexClientMock.Setup(x => x.GetSearchClient(TestCollectionName)).Returns(this._searchClientMock.Object);
this._searchIndexClientMock.Setup(x => x.ServiceName).Returns("TestService");
}

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

namespace Microsoft.SemanticKernel.Connectors.AzureAISearch;

#pragma warning disable SKEXP0020 // VectorStoreMetadata is experimental

/// <summary>
/// Class for accessing the list of collections in a Azure AI Search vector store.
/// </summary>
Expand All @@ -19,8 +21,8 @@ namespace Microsoft.SemanticKernel.Connectors.AzureAISearch;
/// </remarks>
public class AzureAISearchVectorStore : IVectorStore
{
/// <summary>The name of this database for telemetry purposes.</summary>
private const string DatabaseName = "AzureAISearch";
/// <summary>Metadata about vector store.</summary>
private readonly VectorStoreMetadata _metadata;

/// <summary>Azure AI Search client that can be used to manage the list of indices in an Azure AI Search Service.</summary>
private readonly SearchIndexClient _searchIndexClient;
Expand All @@ -39,6 +41,12 @@ public AzureAISearchVectorStore(SearchIndexClient searchIndexClient, AzureAISear

this._searchIndexClient = searchIndexClient;
this._options = options ?? new AzureAISearchVectorStoreOptions();

this._metadata = new()
{
VectorStoreSystemName = "azure.aisearch",
DatabaseName = searchIndexClient.ServiceName
};
}

/// <inheritdoc />
Expand Down Expand Up @@ -75,22 +83,38 @@ public virtual async IAsyncEnumerable<string> ListCollectionNamesAsync([Enumerat
var indexNamesEnumerable = this._searchIndexClient.GetIndexNamesAsync(cancellationToken).ConfigureAwait(false);
var indexNamesEnumerator = indexNamesEnumerable.GetAsyncEnumerator();

var nextResult = await GetNextIndexNameAsync(indexNamesEnumerator).ConfigureAwait(false);
var nextResult = await GetNextIndexNameAsync(indexNamesEnumerator, this._metadata.VectorStoreSystemName).ConfigureAwait(false);
while (nextResult.more)
{
yield return nextResult.name;
nextResult = await GetNextIndexNameAsync(indexNamesEnumerator).ConfigureAwait(false);
nextResult = await GetNextIndexNameAsync(indexNamesEnumerator, this._metadata.VectorStoreSystemName).ConfigureAwait(false);
}
}

/// <inheritdoc />
public object? GetService(Type serviceType, object? serviceKey = null)
{
Verify.NotNull(serviceType);

return
serviceKey is not null ? null :
serviceType == typeof(VectorStoreMetadata) ? this._metadata :
serviceType == typeof(SearchIndexClient) ? this._searchIndexClient :
serviceType.IsInstanceOfType(this) ? this :
null;
}

/// <summary>
/// Helper method to get the next index name from the enumerator with a try catch around the move next call to convert
/// any <see cref="RequestFailedException"/> to <see cref="HttpOperationException"/>, since try catch is not supported
/// around a yield return.
/// </summary>
/// <param name="enumerator">The enumerator to get the next result from.</param>
/// <param name="vectorStoreSystemName">The vector store system name.</param>
/// <returns>A value indicating whether there are more results and the current string if true.</returns>
private static async Task<(string name, bool more)> GetNextIndexNameAsync(ConfiguredCancelableAsyncEnumerable<string>.Enumerator enumerator)
private static async Task<(string name, bool more)> GetNextIndexNameAsync(
ConfiguredCancelableAsyncEnumerable<string>.Enumerator enumerator,
string? vectorStoreSystemName)
{
const string OperationName = "GetIndexNames";

Expand All @@ -103,15 +127,15 @@ public virtual async IAsyncEnumerable<string> ListCollectionNamesAsync([Enumerat
{
throw new VectorStoreOperationException("Call to vector store failed.", ex)
{
VectorStoreType = DatabaseName,
VectorStoreType = vectorStoreSystemName,
OperationName = OperationName
};
}
catch (RequestFailedException ex)
{
throw new VectorStoreOperationException("Call to vector store failed.", ex)
{
VectorStoreType = DatabaseName,
VectorStoreType = vectorStoreSystemName,
OperationName = OperationName
};
}
Expand Down
Loading
Loading