Skip to content
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

Add health check support for Qdrant client #6057

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
<PackageVersion Include="Testcontainers.Milvus" Version="$(TestcontainersPackageVersion)" />
<PackageVersion Include="Testcontainers.Oracle" Version="$(TestcontainersPackageVersion)" />
<PackageVersion Include="Testcontainers.Elasticsearch" Version="$(TestcontainersPackageVersion)" />
<PackageVersion Include="Testcontainers" Version="$(TestcontainersPackageVersion)" />
<!-- playground apps dependencies -->
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Microsoft.Orleans.Clustering.AzureStorage" Version="8.2.0" />
Expand All @@ -185,11 +186,10 @@
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs" Version="6.6.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Storage.Queues" Version="5.5.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.ServiceBus" Version="5.22.0" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.EventHubs" Version="6.3.6"/>
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.EventHubs" Version="6.3.6" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.OpenTelemetry" Version="1.0.0-preview1" />
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.17.4" />
<PackageVersion Include="Microsoft.ApplicationInsights.WorkerService" Version="2.22.0" />

<!-- Pinned versions for Component Governance - Remove when root dependencies are updated -->
<PackageVersion Include="System.Text.Json" Version="8.0.4" />
<PackageVersion Include="Azure.Core" Version="1.43.0" />
Expand Down
2 changes: 1 addition & 1 deletion NuGet.config
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<configuration>
Alirexaa marked this conversation as resolved.
Show resolved Hide resolved
<packageSources>
<clear />
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Aspire.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
<InternalsVisibleTo Include="Aspire.Hosting.Tests" />
<InternalsVisibleTo Include="Aspire.Hosting.Testing.Tests" />
<InternalsVisibleTo Include="Aspire.Hosting.Containers.Tests" />
<InternalsVisibleTo Include="Aspire.Qdrant.Client.Tests"/>
Alirexaa marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@

<ItemGroup>
<Compile Include="..\Common\ConfigurationSchemaAttributes.cs" Link="ConfigurationSchemaAttributes.cs" />
<Compile Include="..\Common\HealthChecksExtensions.cs" Link="HealthChecksExtensions.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Qdrant.Client" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>

</Project>
37 changes: 37 additions & 0 deletions src/Components/Aspire.Qdrant.Client/AspireQdrantExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire;
using Aspire.Qdrant.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Qdrant.Client;

Expand Down Expand Up @@ -78,6 +80,41 @@ private static void AddQdrant(
builder.Services.AddKeyedSingleton(serviceKey, (sp, key) => ConfigureQdrant(sp));
}

if (!settings.DisableHealthChecks)
{
// use http client for register health check
var httpClientName = serviceKey is null ? "qdrant-healthchecks" : $"qdrant-healthchecks_{connectionName}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not implement this in Xabaril/AspNetCore.Diagnostics.HealthChecks#2186? Then everyone can use it, and not just people using the Aspire integration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qdrant-client added health check method via qdrant/qdrant-dotnet#71. the latest version should have this method. I will try to use the client method instead of the current implementation in Aspire. also, I will update Xabaril PR to use this new health check method and we will update the existing aspire client to use the Xabaril package when the new health check package is released.

builder.Services.AddHttpClient(
httpClientName,
client =>
{
if (settings.Endpoint is null)
{
throw new InvalidOperationException(
$"A QdrantClient could not be configured. Ensure valid connection information was provided in 'ConnectionStrings:{connectionName}' or either " +
$"{nameof(settings.Endpoint)} must be provided " +
$"in the '{configurationSectionName}' configuration section.");
}

client.BaseAddress = settings.Endpoint;

if (settings.Key is not null)
{
client.DefaultRequestHeaders.Add("Api-Key", settings.Key);
}
Alirexaa marked this conversation as resolved.
Show resolved Hide resolved
});

var healthCheckName = serviceKey is null ? "Qdrant.Client" : $"Qdrant.Client_{connectionName}";

builder.TryAddHealthCheck(new HealthCheckRegistration(
healthCheckName,
sp => new QdrantHealthCheck(sp.GetRequiredService<IHttpClientFactory>().CreateClient(httpClientName)),
failureStatus: null,
tags: null,
timeout: settings.HealthCheckTimeout > 0 ? TimeSpan.FromMilliseconds(settings.HealthCheckTimeout.Value) : null
));
}

QdrantClient ConfigureQdrant(IServiceProvider serviceProvider)
{
if (settings.Endpoint is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,20 @@
"Client": {
"type": "object",
"properties": {
"DisableHealthChecks": {
"type": "boolean",
"description": "Gets or sets a boolean value that indicates whether the Qdrant client health check is disabled or not.",
"default": false
},
"Endpoint": {
"type": "string",
"format": "uri",
"description": "The endpoint URI string of the Qdrant server to connect to."
},
"HealthCheckTimeout": {
"type": "integer",
"description": "Gets or sets a integer value that indicates the Qdrant health check timeout in milliseconds."
},
"Key": {
"type": "string",
"description": "The API Key of the Qdrant server to connect to."
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
#nullable enable

Aspire.Qdrant.Client.QdrantClientSettings.DisableHealthChecks.get -> bool
Aspire.Qdrant.Client.QdrantClientSettings.DisableHealthChecks.set -> void
Aspire.Qdrant.Client.QdrantClientSettings.HealthCheckTimeout.get -> int?
Aspire.Qdrant.Client.QdrantClientSettings.HealthCheckTimeout.set -> void
13 changes: 13 additions & 0 deletions src/Components/Aspire.Qdrant.Client/QdrantClientSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ public sealed class QdrantClientSettings
/// </summary>
public string? Key { get; set; }

/// <summary>
/// Gets or sets a boolean value that indicates whether the Qdrant client health check is disabled or not.
/// </summary>
/// <value>
/// The default value is <see langword="false"/>.
/// </value>
public bool DisableHealthChecks { get; set; }

/// <summary>
/// Gets or sets a integer value that indicates the Qdrant health check timeout in milliseconds.
/// </summary>
public int? HealthCheckTimeout { get; set; }

internal void ParseConnectionString(string? connectionString)
{
if (Uri.TryCreate(connectionString, UriKind.Absolute, out var uri))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<ItemGroup>
<None Include="$(RepoRoot)src\Components\Aspire.Qdrant.Client\ConfigurationSchema.json" CopyToOutputDirectory="PreserveNewest" />
<Compile Include="$(RepoRoot)src\Aspire.Hosting.Qdrant\QdrantContainerImageTags.cs" />
<PackageReference Include="Testcontainers" />
<ProjectReference Include="..\..\src\Aspire.Hosting\Aspire.Hosting.csproj" />
<ProjectReference Include="..\Aspire.Components.Common.Tests\Aspire.Components.Common.Tests.csproj" />
<ProjectReference Include="..\..\src\Components\Aspire.Qdrant.Client\Aspire.Qdrant.Client.csproj" />
</ItemGroup>
Expand Down
127 changes: 127 additions & 0 deletions tests/Aspire.Qdrant.Client.Tests/AspireQdrantClientExtensionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Components.Common.Tests;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Qdrant.Client;
using Xunit;

namespace Aspire.Qdrant.Client.Tests;

public class AspireQdrantClientExtensionsTest : IClassFixture<QdrantContainerFixture>
{
private const string DefaultConnectionName = "qdrant";

private readonly QdrantContainerFixture _containerFixture;

public AspireQdrantClientExtensionsTest(QdrantContainerFixture containerFixture)
{
_containerFixture = containerFixture;
}

private string DefaultConnectionString =>
RequiresDockerAttribute.IsSupported ? _containerFixture.GetConnectionString() : "Endpoint=http://localhost1:6331;Key=pass";

[Theory]
[InlineData(true)]
[InlineData(false)]
[RequiresDocker]
public async Task AddQdrantClient_HealthCheckShouldBeRegisteredWhenEnabled(bool useKeyed)
{
var key = DefaultConnectionName;

var builder = CreateBuilder(DefaultConnectionString);

if (useKeyed)
{
builder.AddKeyedQdrantClient(key, settings =>
{
settings.DisableHealthChecks = false;
});
}
else
{
builder.AddQdrantClient(DefaultConnectionName, settings =>
{
settings.DisableHealthChecks = false;
});
}

using var host = builder.Build();

var healthCheckService = host.Services.GetRequiredService<HealthCheckService>();

var healthCheckReport = await healthCheckService.CheckHealthAsync();

var healthCheckName = useKeyed ? $"Qdrant.Client_{key}" : "Qdrant.Client";

Assert.Contains(healthCheckReport.Entries, x => x.Key == healthCheckName);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void AddQdrant_HealthCheckShouldNotBeRegisteredWhenDisabled(bool useKeyed)
{
var builder = CreateBuilder(DefaultConnectionString);

if (useKeyed)
{
builder.AddKeyedQdrantClient(DefaultConnectionName, settings =>
{
settings.DisableHealthChecks = true;
});
}
else
{
builder.AddQdrantClient(DefaultConnectionName, settings =>
{
settings.DisableHealthChecks = true;
});
}

using var host = builder.Build();

var healthCheckService = host.Services.GetService<HealthCheckService>();

Assert.Null(healthCheckService);
}

[Fact]
public void CanAddMultipleKeyedServices()
{
var builder = Host.CreateEmptyApplicationBuilder(null);
builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>("ConnectionStrings:qdrant1", "Endpoint=http://localhost1:6331;Key=pass"),
new KeyValuePair<string, string?>("ConnectionStrings:qdrant2", "Endpoint=http://localhost2:6332;Key=pass"),
new KeyValuePair<string, string?>("ConnectionStrings:qdrant3", "Endpoint=http://localhost3:6333;Key=pass"),
]);

builder.AddQdrantClient("qdrant1");
builder.AddKeyedQdrantClient("qdrant2");
builder.AddKeyedQdrantClient("qdrant3");

using var host = builder.Build();

var client1 = host.Services.GetRequiredService<QdrantClient>();
var client2 = host.Services.GetRequiredKeyedService<QdrantClient>("qdrant2");
var client3 = host.Services.GetRequiredKeyedService<QdrantClient>("qdrant3");

Assert.NotSame(client1, client2);
Assert.NotSame(client1, client3);
Assert.NotSame(client2, client3);
}

private static HostApplicationBuilder CreateBuilder(string connectionString)
{
var builder = Host.CreateEmptyApplicationBuilder(null);

builder.Configuration.AddInMemoryCollection([
new KeyValuePair<string, string?>($"ConnectionStrings:{DefaultConnectionName}", connectionString)
]);
return builder;
}
}
37 changes: 0 additions & 37 deletions tests/Aspire.Qdrant.Client.Tests/AspireQdrantHelpers.cs

This file was deleted.

4 changes: 4 additions & 0 deletions tests/Aspire.Qdrant.Client.Tests/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ public class ConfigurationTests
[Fact]
public void EndpointIsNullByDefault()
=> Assert.Null(new QdrantClientSettings().Endpoint);

[Fact]
public void HealthChecksEnabledByDefault() =>
Assert.False(new QdrantClientSettings().DisableHealthChecks);
}
Loading
Loading