Skip to content

Commit

Permalink
Added MySQL HealthChecks Query and Callback support. (#1835)
Browse files Browse the repository at this point in the history
  • Loading branch information
turric4n authored Jul 13, 2023
1 parent d4962bd commit a19515f
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using HealthChecks.MySql;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MySqlConnector;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -9,12 +10,15 @@ namespace Microsoft.Extensions.DependencyInjection;
public static class MySqlHealthCheckBuilderExtensions
{
private const string NAME = "mysql";
internal const string HEALTH_QUERY = "SELECT 1;";

/// <summary>
/// Add a health check for MySql databases.
/// Add a health check for MySQL databases.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="connectionString">The MySql connection string to be used.</param>
/// <param name="connectionString">The MySQL connection string to be used.</param>
/// <param name="healthQuery">The query to be executed.</param>
/// <param name="configure">An optional action to allow additional MySQL specific configuration.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' 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
Expand All @@ -26,14 +30,86 @@ public static class MySqlHealthCheckBuilderExtensions
public static IHealthChecksBuilder AddMySql(
this IHealthChecksBuilder builder,
string connectionString,
string healthQuery = HEALTH_QUERY,
Action<MySqlConnection>? configure = null,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return builder.AddMySql(_ => connectionString, healthQuery, configure, name, failureStatus, tags, timeout);
}

/// <summary>
/// Add a health check for MySQL databases.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="connectionStringFactory">A factory to build the MySQL connection string to use.</param>
/// <param name="healthQuery">The query to be executed.</param>
/// <param name="configure">An optional action to allow additional MySQL specific configuration.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' 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 AddMySql(
this IHealthChecksBuilder builder,
Func<IServiceProvider, string> connectionStringFactory,
string healthQuery = HEALTH_QUERY,
Action<MySqlConnection>? configure = null,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
Guard.ThrowIfNull(connectionStringFactory);

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp =>
{
var options = new MySqlHealthCheckOptions
{
ConnectionString = connectionStringFactory(sp),
CommandText = healthQuery,
Configure = configure,
};
return new MySqlHealthCheck(options);
},
failureStatus,
tags,
timeout));
}

/// <summary>
/// Add a health check for MySQL databases.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="options">Options for health check.</param>
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' 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 AddMySql(
this IHealthChecksBuilder builder,
MySqlHealthCheckOptions options,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
Guard.ThrowIfNull(options);

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
_ => new MySqlHealthCheck(connectionString),
_ => new MySqlHealthCheck(options),
failureStatus,
tags,
timeout));
Expand Down
19 changes: 13 additions & 6 deletions src/HealthChecks.MySql/MySqlHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@ namespace HealthChecks.MySql;
/// </summary>
public class MySqlHealthCheck : IHealthCheck
{
private readonly string _connectionString;
private readonly MySqlHealthCheckOptions _options;

public MySqlHealthCheck(string connectionString)
public MySqlHealthCheck(MySqlHealthCheckOptions options)
{
_connectionString = Guard.ThrowIfNull(connectionString);
Guard.ThrowIfNull(options.ConnectionString, true);
Guard.ThrowIfNull(options.CommandText, true);
_options = options;
}

/// <inheritdoc />
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
using var connection = new MySqlConnection(_connectionString);
using var connection = new MySqlConnection(_options.ConnectionString);

_options.Configure?.Invoke(connection);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);

return await connection.PingAsync(cancellationToken).ConfigureAwait(false)
using var command = connection.CreateCommand();
command.CommandText = _options.CommandText;
object? result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

return _options.HealthCheckResultBuilder == null
? HealthCheckResult.Healthy()
: new HealthCheckResult(context.Registration.FailureStatus, description: $"The {nameof(MySqlHealthCheck)} check fail.");
: _options.HealthCheckResultBuilder(result);
}
catch (Exception ex)
{
Expand Down
31 changes: 31 additions & 0 deletions src/HealthChecks.MySql/MySqlHealthCheckOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MySqlConnector;

namespace HealthChecks.MySql;

/// <summary>
/// Options for <see cref="MySqlHealthCheck"/>.
/// </summary>
public class MySqlHealthCheckOptions
{
/// <summary>
/// The MySQL connection string to be used.
/// </summary>
public string ConnectionString { get; set; } = null!;

/// <summary>
/// The query to be executed.
/// </summary>
public string CommandText { get; set; } = MySqlHealthCheckBuilderExtensions.HEALTH_QUERY;

/// <summary>
/// An optional action executed before the connection is opened in the health check.
/// </summary>
public Action<MySqlConnection>? Configure { get; set; }

/// <summary>
/// An optional delegate to build health check result.
/// </summary>
public Func<object?, HealthCheckResult>? HealthCheckResultBuilder { get; set; }
}
30 changes: 30 additions & 0 deletions test/HealthChecks.MySql.Tests/Functional/MySqlHealthCheckTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,34 @@ public async Task be_unhealthy_when_mysql_server_is_unavailable()

response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable);
}

[Fact]
public async Task be_unhealthy_when_mysql_server_is_unavailable_using_options()
{
var connectionString = "server=255.255.255.255;port=3306;database=information_schema;uid=root;password=Password12!";

var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
var mysqlOptions = new MySqlHealthCheckOptions
{
ConnectionString = connectionString
};
services.AddHealthChecks()
.AddMySql(mysqlOptions, tags: new string[] { "mysql" });
})
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("mysql")
});
});

using var server = new TestServer(webHostBuilder);

var response = await server.CreateRequest("/health").GetAsync().ConfigureAwait(false);

response.StatusCode.ShouldBe(HttpStatusCode.ServiceUnavailable);
}
}
14 changes: 12 additions & 2 deletions test/HealthChecks.MySql.Tests/HealthChecks.MySql.approved.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,24 @@ namespace HealthChecks.MySql
{
public class MySqlHealthCheck : Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck
{
public MySqlHealthCheck(string connectionString) { }
public MySqlHealthCheck(HealthChecks.MySql.MySqlHealthCheckOptions options) { }
public System.Threading.Tasks.Task<Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult> CheckHealthAsync(Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckContext context, System.Threading.CancellationToken cancellationToken = default) { }
}
public class MySqlHealthCheckOptions
{
public MySqlHealthCheckOptions() { }
public string CommandText { get; set; }
public System.Action<MySqlConnector.MySqlConnection>? Configure { get; set; }
public string ConnectionString { get; set; }
public System.Func<object?, Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult>? HealthCheckResultBuilder { get; set; }
}
}
namespace Microsoft.Extensions.DependencyInjection
{
public static class MySqlHealthCheckBuilderExtensions
{
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddMySql(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string connectionString, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddMySql(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, HealthChecks.MySql.MySqlHealthCheckOptions options, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddMySql(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, System.Func<System.IServiceProvider, string> connectionStringFactory, string healthQuery = "SELECT 1;", System.Action<MySqlConnector.MySqlConnection>? configure = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
public static Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder AddMySql(this Microsoft.Extensions.DependencyInjection.IHealthChecksBuilder builder, string connectionString, string healthQuery = "SELECT 1;", System.Action<MySqlConnector.MySqlConnection>? configure = null, string? name = null, Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus? failureStatus = default, System.Collections.Generic.IEnumerable<string>? tags = null, System.TimeSpan? timeout = default) { }
}
}

0 comments on commit a19515f

Please sign in to comment.