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

Use MySqlDataSource in AddMySql by default #2096

Merged
merged 6 commits into from
Dec 11, 2023
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
Expand Up @@ -10,14 +10,50 @@ 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.
/// </summary>
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
/// <param name="dataSourceFactory">An optional factory to create the <see cref="MySqlDataSource"/>. By default, one will be retrieved from the service collection.</param>
/// <param name="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</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, MySqlDataSource>? dataSourceFactory = null,
string? healthQuery = null,
Action<MySqlConnection>? configure = null,
string? name = default,
HealthStatus? failureStatus = default,
IEnumerable<string>? tags = default,
TimeSpan? timeout = default)
{
return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp => new MySqlHealthCheck(new(dataSourceFactory?.Invoke(sp) ?? sp.GetRequiredService<MySqlDataSource>())
{
CommandText = healthQuery,
Configure = configure,
}),
failureStatus,
tags,
timeout));
}

/// <summary>
/// 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="healthQuery">The query to be executed.</param>
/// <param name="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</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">
Expand All @@ -30,22 +66,28 @@ public static class MySqlHealthCheckBuilderExtensions
public static IHealthChecksBuilder AddMySql(
this IHealthChecksBuilder builder,
string connectionString,
string healthQuery = HEALTH_QUERY,
string? healthQuery = null,
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);
Guard.ThrowIfNull(connectionString, throwOnEmptyString: true);

return builder.AddMySql(new(connectionString)
{
CommandText = healthQuery,
Configure = 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="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</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">
Expand All @@ -58,7 +100,7 @@ public static IHealthChecksBuilder AddMySql(
public static IHealthChecksBuilder AddMySql(
this IHealthChecksBuilder builder,
Func<IServiceProvider, string> connectionStringFactory,
string healthQuery = HEALTH_QUERY,
string? healthQuery = null,
Action<MySqlConnection>? configure = null,
string? name = default,
HealthStatus? failureStatus = default,
Expand All @@ -69,16 +111,11 @@ public static IHealthChecksBuilder AddMySql(

return builder.Add(new HealthCheckRegistration(
name ?? NAME,
sp =>
sp => new MySqlHealthCheck(new(connectionStringFactory(sp))
{
var options = new MySqlHealthCheckOptions
{
ConnectionString = connectionStringFactory(sp),
CommandText = healthQuery,
Configure = configure,
};
return new MySqlHealthCheck(options);
},
CommandText = healthQuery,
Configure = configure,
}),
failureStatus,
tags,
timeout));
Expand Down
33 changes: 24 additions & 9 deletions src/HealthChecks.MySql/MySqlHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public class MySqlHealthCheck : IHealthCheck

public MySqlHealthCheck(MySqlHealthCheckOptions options)
{
Guard.ThrowIfNull(options.ConnectionString, true);
Guard.ThrowIfNull(options.CommandText, true);
Guard.ThrowIfNull(options);
if (options.DataSource is null && options.ConnectionString is null)
throw new InvalidOperationException("One of options.DataSource or options.ConnectionString must be specified.");
if (options.DataSource is not null && options.ConnectionString is not null)
throw new InvalidOperationException("Only one of options.DataSource or options.ConnectionString must be specified.");
_options = options;
Comment on lines +15 to 20
Copy link
Collaborator

Choose a reason for hiding this comment

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

We don't need these checks anymore, as the public ctors ensure that the state is always valid.

Suggested change
Guard.ThrowIfNull(options);
if (options.DataSource is null && options.ConnectionString is null)
throw new InvalidOperationException("One of options.DataSource or options.ConnectionString must be specified.");
if (options.DataSource is not null && options.ConnectionString is not null)
throw new InvalidOperationException("Only one of options.DataSource or options.ConnectionString must be specified.");
_options = options;
_options = Guard.ThrowIfNull(options);

Copy link
Collaborator

Choose a reason for hiding this comment

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

For some reason I can't apply this suggestion. I am going to merge it now since it's not blocking

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

Expand All @@ -22,18 +25,30 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
{
try
{
using var connection = new MySqlConnection(_options.ConnectionString);
using var connection = _options.DataSource is not null ?
_options.DataSource.CreateConnection() :
new MySqlConnection(_options.ConnectionString);

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

using var command = connection.CreateCommand();
command.CommandText = _options.CommandText;
object? result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
if (_options.CommandText is { } commandText)
{
using var command = connection.CreateCommand();
command.CommandText = _options.CommandText;
object? result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);

return _options.HealthCheckResultBuilder == null
? HealthCheckResult.Healthy()
: _options.HealthCheckResultBuilder(result);
return _options.HealthCheckResultBuilder == null
? HealthCheckResult.Healthy()
: _options.HealthCheckResultBuilder(result);
}
else
{
var success = await connection.PingAsync(cancellationToken).ConfigureAwait(false);
return _options.HealthCheckResultBuilder is null
? (success ? HealthCheckResult.Healthy() : new HealthCheckResult(context.Registration.FailureStatus)) :
_options.HealthCheckResultBuilder(success);
}
}
catch (Exception ex)
{
Expand Down
48 changes: 43 additions & 5 deletions src/HealthChecks.MySql/MySqlHealthCheckOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MySqlConnector;

Expand All @@ -8,16 +7,55 @@ namespace HealthChecks.MySql;
/// Options for <see cref="MySqlHealthCheck"/>.
/// </summary>
public class MySqlHealthCheckOptions
{
{
/// <summary>
/// Creates an instance of <see cref="MySqlHealthCheckOptions"/>.
/// </summary>
/// <param name="dataSource">The <see cref="MySqlDataSource" /> to be used.</param>
/// <remarks>
/// Depending on how the <see cref="MySqlDataSource" /> was configured, the connections it hands out may be pooled.
/// That is why it should be the exact same <see cref="MySqlDataSource" /> that is used by other parts of your app.
/// </remarks>
public MySqlHealthCheckOptions(MySqlDataSource dataSource)
{
DataSource = Guard.ThrowIfNull(dataSource);
}

/// <summary>
/// Creates an instance of <see cref="MySqlHealthCheckOptions"/>.
/// </summary>
/// <param name="connectionString">The MySQL connection string to be used.</param>
/// <remarks>
/// <see cref="MySqlDataSource"/> supports additional configuration beyond the connection string, such as logging and naming pools for diagnostics.
/// To specify a data source, use <see cref=" MySqlDataSourceBuilder"/> and the <see cref="MySqlHealthCheckOptions(MySqlDataSource)"/> constructor.
/// </remarks>
public MySqlHealthCheckOptions(string connectionString)
{
ConnectionString = Guard.ThrowIfNull(connectionString, throwOnEmptyString: true);
}

/// <summary>
/// The MySQL data source to be used.
/// </summary>
/// <remarks>
/// Depending on how the <see cref="MySqlDataSource" /> was configured, the connections it hands out may be pooled.
/// That is why it should be the exact same <see cref="MySqlDataSource" /> that is used by other parts of your app.
/// </remarks>
public MySqlDataSource? DataSource { get; }

/// <summary>
/// The MySQL connection string to be used.
/// The MySQL connection string to be used, if <see cref="DataSource"/> isn't set.
/// </summary>
public string ConnectionString { get; set; } = null!;
/// <remarks>
/// <see cref="MySqlDataSource"/> supports additional configuration beyond the connection string, such as logging and naming pools for diagnostics.
/// To specify a data source, use <see cref=" MySqlDataSourceBuilder"/> and the <see cref="MySqlHealthCheckOptions(MySqlDataSource)"/> constructor.
/// </remarks>
public string? ConnectionString { get; }

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

/// <summary>
/// An optional action executed before the connection is opened in the health check.
Expand Down
59 changes: 59 additions & 0 deletions src/HealthChecks.MySql/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
## MySQL Health Check

This health check verifies the ability to communicate with a MySQL Server.
It uses the provided [MySqlDataSource](https://mysqlconnector.net/api/mysqlconnector/mysqldatasourcetype/) or a connection string to connect to the server.

### Defaults

By default, the `MySqlDataSource` instance is resolved from service provider.
(This should be the same as the instance being used by the application; do not create a new `MySqlDataSource` just for the health check.)
The health check will send a MySQL "ping" packet to the server to verify connectivity.

```csharp
builder.Services
.AddMySqlDataSource(builder.Configuration.GetConnectionString("mysql")) // using the MySqlConnector.DependencyInjection package
.AddHealthChecks().AddMySql();
```

### Connection String

You can also specify a connection string directly:

```csharp
builder.Services.AddHealthChecks().AddMySql(connectionString: "Server=...;User Id=...;Password=...");
```

This can be useful if you're not using `MySqlDataSource` in your application.

### Customization

You can additionally add the following parameters:

- `healthQuery`: A query to run against the server. If `null` (the default), the health check will send a MySQL "ping" packet to the server.
- `configure`: An action to configure the `MySqlConnection` object. This is called after the `MySqlConnection` is created but before the connection is opened.
- `name`: The health check name. The default is `mysql`.
- `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.

```csharp
builder.Services
.AddMySqlDataSource(builder.Configuration.GetConnectionString("mysql"))
.AddHealthChecks().AddMySql(
healthQuery: "SELECT 1;",
configure: conn => conn.ConnectTimeout = 3,
name: "MySQL"
);
```

### Breaking changes

In previous versions, `MySqlHealthCheck` defaulted to testing connectivity by sending a `SELECT 1;` query to the server.
It has been changed to send a more efficient "ping" packet instead.
To restore the previous behavior, specify `healthQuery: "SELECT 1;"` when registering the health check.

While not a breaking change, it's now preferred to use `MySqlDataSource` instead of a connection string.
This allows the health check to use the same connection pool as the rest of the application.
This can be achieved by calling the `.AddMySql()` overload that has no required parameters.
The health check assumes that a `MySqlDataSource` instance has been registered with the service provider and will retrieve it automatically.

bgrainger marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using MySqlConnector;

namespace HealthChecks.MySql.Tests.DependencyInjection;

public class mysql_registration_should
Expand All @@ -18,6 +20,7 @@ public void add_health_check_when_properly_configured()
registration.Name.ShouldBe("mysql");
check.ShouldBeOfType<MySqlHealthCheck>();
}

[Fact]
public void add_named_health_check_when_properly_configured()
{
Expand All @@ -34,4 +37,22 @@ public void add_named_health_check_when_properly_configured()
registration.Name.ShouldBe("my-mysql-group");
check.ShouldBeOfType<MySqlHealthCheck>();
}

[Fact]
public void add_health_check_for_data_source()
{
var services = new ServiceCollection();
services
.AddMySqlDataSource("Server=example")
.AddHealthChecks().AddMySql();

using var serviceProvider = services.BuildServiceProvider();
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();

var registration = options.Value.Registrations.First();
var check = registration.Factory(serviceProvider);

registration.Name.ShouldBe("mysql");
check.ShouldBeOfType<MySqlHealthCheck>();
}
}
35 changes: 30 additions & 5 deletions test/HealthChecks.MySql.Tests/Functional/MySqlHealthCheckTests.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
using System.Net;
using MySqlConnector;

namespace HealthChecks.MySql.Tests.Functional;

public class mysql_healthcheck_should
{
[Fact]
public async Task be_healthy_when_mysql_server_is_available()
public async Task be_healthy_when_mysql_server_is_available_using_data_source()
{
var connectionString = "server=localhost;port=3306;database=information_schema;uid=root;password=Password12!";

var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
services
.AddMySqlDataSource(connectionString)
bgrainger marked this conversation as resolved.
Show resolved Hide resolved
.AddHealthChecks().AddMySql(tags: new string[] { "mysql" });
})
.Configure(app =>
{
app.UseHealthChecks("/health", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("mysql")
});
});

using var server = new TestServer(webHostBuilder);

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

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

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

Expand Down Expand Up @@ -64,10 +92,7 @@ public async Task be_unhealthy_when_mysql_server_is_unavailable_using_options()
var webHostBuilder = new WebHostBuilder()
.ConfigureServices(services =>
{
var mysqlOptions = new MySqlHealthCheckOptions
{
ConnectionString = connectionString
};
var mysqlOptions = new MySqlHealthCheckOptions(connectionString);
services.AddHealthChecks()
.AddMySql(mysqlOptions, tags: new string[] { "mysql" });
})
Expand Down
4 changes: 4 additions & 0 deletions test/HealthChecks.MySql.Tests/HealthChecks.MySql.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="MySqlConnector.DependencyInjection" Version="2.3.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\HealthChecks.MySql\HealthChecks.MySql.csproj" />
</ItemGroup>
Expand Down
Loading