Skip to content

Commit

Permalink
moving the healthchecks to a Extensions issue #84
Browse files Browse the repository at this point in the history
  • Loading branch information
kdcllc committed Oct 24, 2019
1 parent f592903 commit 7251cf5
Show file tree
Hide file tree
Showing 17 changed files with 944 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This repo contains several projects that provide with extended functionality for
4. [`Bet.Extensions.Hosting`](./src/Bet.Extensions.Hosting/README.md) - extends generic functionality for `IHost`.
5. [`Bet.Extensions.AzureVault`](./src/Bet.Extensions.AzureVault/README.md) - includes Azure Vault functionality.
6. [`Bet.Extensions.AzureStorage`](./src/Bet.Extensions.AzureStorage/README.md) - includes MSI and regular access to Azure Storage Blob or Queue.
7. [`Bet.Extensions.HealthChecks`](./src/Bet.Extensions.HealthChecks/README.md) - many useful HealChecks for Kubernetes.

## AspNetCore specific functionality

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Azure.Storage;
using Microsoft.Azure.Storage.Auth;
using Microsoft.Azure.Storage.Blob;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Bet.Extensions.HealthChecks.AzureBlobStorage
{
public class AzureBlobStorageHealthCheck : IHealthCheck
{
private IOptionsMonitor<StorageAccountOptions> _options;
private ILogger<AzureBlobStorageHealthCheck> _logger;

public AzureBlobStorageHealthCheck(
IOptionsMonitor<StorageAccountOptions> options,
ILogger<AzureBlobStorageHealthCheck> logger)
{
_options = options;
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var checkName = context.Registration.Name;
var options = _options.Get(checkName);
var fullCheckName = $"{checkName}-{options?.ContainerName}";

try
{
var cloudStorageAccount = await GetStorageAccountAsync(options);
var cloudBlobClient = cloudStorageAccount.CreateCloudBlobClient();

var cloudBlobContainer = cloudBlobClient.GetContainerReference(options.ContainerName);

_logger.LogInformation("[HealthCheck][{healthCheckName}]", fullCheckName);

await cloudBlobContainer.CreateIfNotExistsAsync(cancellationToken);

return new HealthCheckResult(HealthStatus.Healthy, fullCheckName);
}
catch (Exception ex)
{
_logger.LogError(ex, "[HealthCheck][{healthCheckName}]", fullCheckName);
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}

private async Task<NewTokenAndFrequency> TokenRenewerAsync(object state, CancellationToken cancellationToken)
{
var sw = ValueStopwatch.StartNew();

cancellationToken.ThrowIfCancellationRequested();

var (authResult, next) = await GetToken(state);

_logger.LogInformation("Azure Storage Authentication duration: {0}", sw.GetElapsedTime().TotalSeconds);

if (next.Ticks < 0)
{
next = default;

_logger.LogInformation("Azure Storage Authentication Renewing Token...");

var swr = ValueStopwatch.StartNew();

(authResult, next) = await GetToken(state);

_logger.LogInformation("Azure Storage Authentication Renewing Token duration: {0}", swr.GetElapsedTime().TotalSeconds);
}

// Return the new token and the next refresh time.
return new NewTokenAndFrequency(authResult.AccessToken, next);
}

// https://github.com/MicrosoftDocs/azure-docs/blob/941ccc038829a0be5fa8515ffba9956cfc02a5e6/articles/storage/common/storage-auth-aad-msi.md
private async Task<(AppAuthenticationResult result, TimeSpan ticks)> GetToken(object state)
{
// Specify the resource ID for requesting Azure AD tokens for Azure Storage.
const string StorageResource = "https://storage.azure.com/";

// Use the same token provider to request a new token.
var authResult = await ((AzureServiceTokenProvider)state).GetAuthenticationResultAsync(StorageResource);

// Renew the token 5 minutes before it expires.
var next = authResult.ExpiresOn - DateTimeOffset.UtcNow - TimeSpan.FromMinutes(5);

// debug purposes
// var next = (authResult.ExpiresOn - authResult.ExpiresOn) + TimeSpan.FromSeconds(15);
return (authResult, next);
}

private async Task<CloudStorageAccount> GetStorageAccountAsync(StorageAccountOptions options, CancellationToken cancellationToken = default)
{
CloudStorageAccount account;

if (!string.IsNullOrEmpty(options.ConnectionString)
&& CloudStorageAccount.TryParse(options.ConnectionString, out var cloudStorageAccount))
{
account = cloudStorageAccount;

_logger.LogInformation("Azure Storage Authentication with ConnectionString.");
}
else if (!string.IsNullOrEmpty(options.Name)
&& string.IsNullOrEmpty(options.Token))
{
// Get the initial access token and the interval at which to refresh it.
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var tokenAndFrequency = await TokenRenewerAsync(
azureServiceTokenProvider,
cancellationToken);

// Create storage credentials using the initial token, and connect the callback function
// to renew the token just before it expires
#pragma warning disable CA2000 // Dispose objects before losing scope
var tokenCredential = new TokenCredential(
tokenAndFrequency.Token,
TokenRenewerAsync,
azureServiceTokenProvider,
tokenAndFrequency.Frequency.Value);
#pragma warning restore CA2000 // Dispose objects before losing scope

var storageCredentials = new StorageCredentials(tokenCredential);

account = new CloudStorageAccount(storageCredentials, options.Name, string.Empty, true);

_logger.LogInformation("Azure Storage Authentication with MSI Token.");
}
else if (!string.IsNullOrEmpty(options.Name)
&& !string.IsNullOrEmpty(options.Token))
{
account = new CloudStorageAccount(new StorageCredentials(options.Token), options.Name, true);
_logger.LogInformation("Azure Storage Authentication with SAS Token.");
}
else
{
throw new ArgumentException($"One of the following must be set: '{options.ConnectionString}' or '{options.Name}'!");
}

return account;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Azure.Services.AppAuthentication;

namespace Bet.Extensions.HealthChecks.AzureBlobStorage
{
public class StorageAccountOptions
{
/// <summary>
/// Azure Storage Connection String.
/// </summary>
public string ConnectionString { get; set; }

/// <summary>
/// Name of the Storage Account. Used with <see cref="AzureServiceTokenProvider"/>.
/// This is enable only if ConnectionString is empty or null.
/// </summary>
public string Name { get; set; }

/// <summary>
/// SAS Token used with Name of the Storage Account.
/// </summary>
public string Token { get; set; }

internal string ContainerName { get; set; }
}
}
13 changes: 13 additions & 0 deletions src/Bet.Extensions.HealthChecks/Bet.Extensions.HealthChecks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>

<PropertyGroup>
<Description>The collection of the HealthChecks functionality for Worker Pods.</Description>
<PackageTags>dotnetcore, worker, healthcheck, hosting, extensions</PackageTags>
</PropertyGroup>


<ItemGroup>
<PackageReference Include="Microsoft.Azure.Services.AppAuthentication" />
<PackageReference Include="Microsoft.Azure.Storage.Blob" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Bet.Extensions\Bet.Extensions.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Bet.Extensions.HealthChecks.CertificateCheck
{
public class SslCertificateHealthCheck : IHealthCheck
{
private readonly IHttpClientFactory _httpClientFactory;

public SslCertificateHealthCheck(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var checkName = context.Registration.Name;
var httpClient = _httpClientFactory.CreateClient(checkName);

using (var response = await httpClient.GetAsync("/", cancellationToken))
{
response.EnsureSuccessStatusCode();
}

return new HealthCheckResult(HealthStatus.Healthy, $"{checkName}-{httpClient.BaseAddress}");
}
catch (Exception ex)
{
return new HealthCheckResult(context.Registration.FailureStatus, exception: ex);
}
}
}
}
Loading

0 comments on commit 7251cf5

Please sign in to comment.