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

Remove private static cache from RabbitMQHealthCheck #2343

Merged
merged 4 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
using HealthChecks.RabbitMQ;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using RabbitMQ.Client;

namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Extension methods to configure <see cref="RabbitMQHealthCheck"/>.
/// </summary>
public static class RabbitMQHealthCheckBuilderExtensions
{
private const string NAME = "rabbitmq";

/// <summary>
/// Add a health check for RabbitMQ services using connection string (amqp uri).
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
string rabbitConnectionString,
SslOption? sslOption = default,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return builder.AddRabbitMQ(new Uri(rabbitConnectionString), sslOption, name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a health check for RabbitMQ services using connection string (amqp uri).
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="rabbitConnectionString">The RabbitMQ connection string to be used.</param>
/// <param name="sslOption">The RabbitMQ ssl options. Optional. If <c>null</c>, the ssl option will counted as disabled and not used.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Uri rabbitConnectionString,
SslOption? sslOption = default,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions
{
ConnectionUri = rabbitConnectionString,
Ssl = sslOption
};

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
new RabbitMQHealthCheck(options),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services using <see cref="IConnection"/> from service provider
/// or <see cref="IConnectionFactory"/> from service provider if none is found. At least one must be configured.
/// </summary>
/// <remarks>
/// This method shouldn't be called more than once.
/// Each subsequent call will create a new connection, which overrides the previous ones.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
builder.Services.AddSingleton(sp =>
{
var connection = sp.GetService<IConnection>();
var connectionFactory = sp.GetService<IConnectionFactory>();

if (connection != null)
{
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { Connection = connection });
}
else if (connectionFactory != null)
{
return new RabbitMQHealthCheck(new RabbitMQHealthCheckOptions { ConnectionFactory = connectionFactory });
}
else
{
throw new ArgumentException($"Either an IConnection or IConnectionFactory must be registered with the service provider");
}
});

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => sp.GetRequiredService<RabbitMQHealthCheck>(),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services.
/// </summary>
/// <remarks>
/// <paramref name="setup"/> will be called each time the healthcheck route is requested. However
/// the created <see cref="IConnection"/> will be reused.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action to configure the RabbitMQ setup.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Action<RabbitMQHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions();
setup?.Invoke(options);

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
new RabbitMQHealthCheck(options),
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for RabbitMQ services.
/// </summary>
/// <remarks>
/// <paramref name="setup"/> will be called the first time the healthcheck route is requested. However
/// the created <see cref="IConnection"/> will be reused.
/// </remarks>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="setup">The action to configure the RabbitMQ setup with <see cref="IServiceProvider"/>.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'rabbitmq' will be used for the name.</param>
/// <param name="failureStatus">
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
/// </param>
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
/// <returns>The specified <paramref name="builder"/>.</returns>
public static IHealthChecksBuilder AddRabbitMQ(
this IHealthChecksBuilder builder,
Action<IServiceProvider, RabbitMQHealthCheckOptions>? setup,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
var options = new RabbitMQHealthCheckOptions();

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp =>
{
if (!options.AlreadyConfiguredByHealthCheckRegistrationCall)
{
setup?.Invoke(sp, options);
options.AlreadyConfiguredByHealthCheckRegistrationCall = true;
}

return new RabbitMQHealthCheck(options);
},
failureStatus,
tags,
timeout));
}
}
4 changes: 0 additions & 4 deletions src/HealthChecks.Rabbitmq.v6/HealthChecks.Rabbitmq.v6.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
<PackageReference Include="RabbitMQ.Client" VersionOverride="[6.8.1,7.0.0)" />

<Compile Include="../HealthCheckResultTask.cs" />
<Compile Include="../HealthChecks.Rabbitmq/DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" Link="DependencyInjection/RabbitMQHealthCheckBuilderExtensions.cs" />
<Compile Include="../HealthChecks.Rabbitmq/RabbitMQHealthCheckOptions.cs" />

<None Include="../HealthChecks.Rabbitmq/README.md" Pack="true" PackagePath="\" />
</ItemGroup>

</Project>
91 changes: 91 additions & 0 deletions src/HealthChecks.Rabbitmq.v6/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# RabbitMQ Health Check

This health check verifies the ability to communicate with a RabbitMQ server

## Example Usage

With all of the following examples, you can additionally add the following parameters:

- `name`: The health check name. Default if not specified is `rabbitmq`.
- `failureStatus`: The `HealthStatus` that should be reported when the health check fails. Default is `HealthStatus.Unhealthy`.
- `tags`: A list of tags that can be used to filter sets of health checks.
- `timeout`: A `System.TimeSpan` representing the timeout of the check.

### Basic

This will create a new `IConnection` and reuse on every request to get the health check result. Use
the extension method where you provide the `Uri` to connect with. You can optionally set the `SslOption` if needed.
IConnection created with this option use UseBackgroundThreadsForIO by default in order to gracefully shutdown on non reference IConnection by ServiceCollection.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHealthChecks()
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host1/vhost")
.AddRabbitMQ(rabbitConnectionString: "amqps://user:pass@host2/vhost");
}
```

### Dependency Injected `IConnection`

As per [RabbitMQ docs](https://www.rabbitmq.com/connections.html) and its suggestions on
[high connectivity churn](https://www.rabbitmq.com/networking.html#dealing-with-high-connection-churn), connections are meant to be long lived.
Ideally, this should be configured as a singleton.

If you are sharing a single connection for every time a health check is requested,
you must ensure automatic recovery is enable so that the connection can be re-established if lost.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IConnection>(sp =>
{
var factory = new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
};
return factory.CreateConnection();
})
.AddHealthChecks()
.AddRabbitMQ();
}
```

Alternatively, you can specify the connection to use with a factory function given the `IServiceProvider`.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddHealthChecks()
.AddRabbitMQ(sp =>
{
var factory = new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
};
return factory.CreateConnection();
});
}
```

Or you register IConnectionFactory and then the healthcheck will create a single connection for that one.

```csharp
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IConnectionFactory>(sp =>
new ConnectionFactory
{
Uri = new Uri("amqps://user:pass@host/vhost"),
AutomaticRecoveryEnabled = true
})
.AddHealthChecks()
.AddRabbitMQ();
}
```
Loading
Loading