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

psp-7417 add health checks for Major pims services. #4025

Merged
merged 2 commits into from
May 17, 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
36 changes: 36 additions & 0 deletions source/backend/api/Helpers/Healthchecks/PimsCdogsHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Api.Repositories.Cdogs;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsCdogsHealthcheck : IHealthCheck
{
private readonly IDocumentGenerationRepository _generationRepository;

public PimsCdogsHealthcheck(IDocumentGenerationRepository generationRepository)
{
_generationRepository = generationRepository;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{

var fileTypes = await _generationRepository.TryGetFileTypesAsync();
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm focusing on real, but low overhead methods for these health checks.

if (fileTypes.HttpStatusCode != System.Net.HttpStatusCode.OK || fileTypes.Payload == null || fileTypes.Payload.Dictionary.Count == 0)
{
return new HealthCheckResult(HealthStatus.Unhealthy, $"received invalid file types response from CDOGS");
}
}
catch (Exception e)
{
return new HealthCheckResult(HealthStatus.Degraded, $"Cdogs error response: {e.Message} {e.StackTrace}");
}
return HealthCheckResult.Healthy();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsDatabaseHealtcheck : IHealthCheck
public class PimsDatabaseHealthcheck : IHealthCheck
{
private const string DefaultProbeExpectedResult = "SQL_Latin1_General_CP1_CI_AS";

public PimsDatabaseHealtcheck(string connectionString)
public PimsDatabaseHealthcheck(string connectionString)
: this(connectionString, probeExpectedResult: DefaultProbeExpectedResult)
{
}

public PimsDatabaseHealtcheck(string connectionString, string probeExpectedResult)
public PimsDatabaseHealthcheck(string connectionString, string probeExpectedResult)
{
ConnectionString = connectionString;
ProbeExpectedResult = probeExpectedResult;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Core.Exceptions;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsExternalApiHealthcheck : IHealthCheck
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The idea here is that this is a generic health check that we can use for any external, unauthenticated api service.

{
private readonly string url;
private readonly int statusCode;

public PimsExternalApiHealthcheck(string url)
: this(url, statusCode: (int)HttpStatusCode.OK)
{
}

public PimsExternalApiHealthcheck(IConfigurationSection section)
{
this.url = section["Url"];
this.statusCode = int.Parse(section["StatusCode"]);
}

public PimsExternalApiHealthcheck(string url, int statusCode)
{
this.url = url;
this.statusCode = statusCode;
}

public string ConnectionString { get; }

public string ProbeExpectedResult { get; }

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
return CheckExternalApi(context, cancellationToken);
}

protected async Task<HealthCheckResult> CheckExternalApi(HealthCheckContext context, CancellationToken cancellationToken = default)
{
using (var client = new HttpClient())
{
try
{
var result = await client.GetAsync(url, cancellationToken);

if ((int)result.StatusCode != statusCode)
{
string errorContent = await result.Content.ReadAsStringAsync(cancellationToken);
return new HealthCheckResult(HealthStatus.Degraded, $"http response: {result.StatusCode} does not match: {statusCode}. message: {errorContent}");
}
}
catch (HttpClientRequestException ex)
{
return new HealthCheckResult(status: context.Registration.FailureStatus, exception: ex);
}
}

return HealthCheckResult.Healthy();
}
}
}
39 changes: 39 additions & 0 deletions source/backend/api/Helpers/Healthchecks/PimsGeocoderHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Geocoder;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsGeocoderHealthcheck : IHealthCheck
{
private readonly IGeocoderService _geocoderService;
private readonly string _address;

public PimsGeocoderHealthcheck(IConfiguration configuration, IGeocoderService geocoderService)
{
_geocoderService = geocoderService;
_address = configuration.GetSection("HealthChecks:Geocoder:Address").Value;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var sites = await _geocoderService.GetSiteAddressesAsync(_address);
if (sites == null || !sites.Features.Any())
{
return new HealthCheckResult(HealthStatus.Unhealthy, $"received invalid file types response from Geocoder");
}
}
catch (Exception e)
{
return new HealthCheckResult(HealthStatus.Degraded, $"Mayan error response: {e.Message}");
}
return HealthCheckResult.Healthy();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Core.Exceptions;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsGeoserverHealthCheck : IHealthCheck
{
private readonly IConfiguration _proxyOptions;
private readonly string _url;
private readonly int _statusCode;

public PimsGeoserverHealthCheck(IConfiguration section)
{
_proxyOptions = section.GetSection("Geoserver");
_url = section.GetValue<string>("HealthChecks:Geoserver:Url");
_statusCode = section.GetValue<int>("HealthChecks:Geoserver:StatusCode");
}

public PimsGeoserverHealthCheck(IConfiguration section, string url, int statusCode)
{
_proxyOptions = section;
_url = url;
_statusCode = statusCode;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
using (var client = new HttpClient())
{
try
{
var basicAuth = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes($"{_proxyOptions.GetValue<string>("ServiceUser")}:{_proxyOptions.GetValue<string>("ServicePassword")}"));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Will log some tech debt for this - currently all of the proxy logic is directly in the controller for geoserver - as a result the auth logic needs to be reproduced here. Would be nice to have that in a dependency injectable service so that the healthcheck can reuse the exact logic used by the endpoint (this puts this implementation at risk of drifting from the functional implementation).

client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuth);
var result = await client.GetAsync($"{_proxyOptions.GetValue<string>("ProxyUrl")}{_url}", cancellationToken);

if (result.StatusCode != HttpStatusCode.OK)
{
string errorContent = await result.Content.ReadAsStringAsync(cancellationToken);
return new HealthCheckResult(HealthStatus.Degraded, $"http response: {result.StatusCode} does not match: {_statusCode}. message: {errorContent}");
}
}
catch (HttpClientRequestException ex)
{
return new HealthCheckResult(status: context.Registration.FailureStatus, exception: ex);
}
}

return HealthCheckResult.Healthy();
}
}
}
38 changes: 38 additions & 0 deletions source/backend/api/Helpers/Healthchecks/PimsLtsaHealthcheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Core.Exceptions;
using Pims.Ltsa;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsLtsaHealthcheck : IHealthCheck
{
private readonly int _pid;
private readonly ILtsaService _ltsaService;

public PimsLtsaHealthcheck(IConfiguration configuration, ILtsaService ltsaService)
{
_pid = int.Parse(configuration.GetSection("HealthChecks:Ltsa:Pid").Value);
_ltsaService = ltsaService;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var titleSummary = await _ltsaService.GetTitleSummariesAsync(_pid);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the lowest impact rest endpoint ltsa has - the only one that doesn't involve generating a document as a result of the request.

if (titleSummary.TitleSummaries.Count == 0)
{
return new HealthCheckResult(HealthStatus.Unhealthy, $"received invalid title summary response for pid: {_pid}");
}
}
catch(LtsaException e)
{
return new HealthCheckResult(HealthStatus.Degraded, $"LTSA error response: {e.Message}");
}
return HealthCheckResult.Healthy();
}
}
}
35 changes: 35 additions & 0 deletions source/backend/api/Helpers/Healthchecks/PimsMayanHealthCheck.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Pims.Api.Repositories.Mayan;

namespace Pims.Api.Helpers.Healthchecks
{
public class PimsMayanHealthcheck : IHealthCheck
{
private readonly IEdmsDocumentRepository _documentRepository;

public PimsMayanHealthcheck(IEdmsDocumentRepository documentRepository)
{
_documentRepository = documentRepository;
}

public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var documentTypes = await _documentRepository.TryGetDocumentTypesAsync(string.Empty, 1, 1);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This seemed like a realistic choice from the rest spec, if there is a better choice let me know.

if (documentTypes.HttpStatusCode != System.Net.HttpStatusCode.OK || documentTypes.Payload == null || documentTypes.Payload.Count == 0)
{
return new HealthCheckResult(HealthStatus.Unhealthy, $"received invalid mayan response for document types");
}
}
catch (Exception e)
{
return new HealthCheckResult(HealthStatus.Degraded, $"Mayan error response: {e.Message}");
}
return HealthCheckResult.Healthy();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@

namespace Pims.Api.Helpers.HealthChecks
{
public class PimsMetricsHealthCheck : IHealthCheck
public class PimsMetricsHealthcheck : IHealthCheck
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

just standardizing naming.

{
private static readonly Gauge AppDeploymentInfo = Metrics.CreateGauge("api_deployment_info", "Deployment information of the running PSP application", labelNames: new[] { "app_version", "db_version", "runtime_version" });

public PimsMetricsHealthCheck(string connectionString)
public PimsMetricsHealthcheck(string connectionString)
{
ConnectionString = connectionString;
}
Expand Down
10 changes: 7 additions & 3 deletions source/backend/api/Services/DocumentGenerationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Pims.Api.Constants;
using Pims.Api.Models.Cdogs;
using Pims.Api.Models.CodeTypes;

using Pims.Api.Models.Requests.Http;
using Pims.Api.Repositories.Cdogs;
using Pims.Av;
using Pims.Core.Http.Configuration;
using Pims.Dal.Helpers.Extensions;
using Pims.Dal.Security;

Expand All @@ -28,19 +30,21 @@ public class DocumentGenerationService : BaseService, IDocumentGenerationService
private readonly IFormDocumentService _formDocumentService;
private readonly IDocumentService _documentService;
private readonly IAvService avService;
private readonly IOptionsMonitor<AuthClientOptions> keycloakOptions;

public DocumentGenerationService(
ClaimsPrincipal user,
ILogger<DocumentGenerationService> logger,
IDocumentGenerationRepository documentGenerationRepository,
IFormDocumentService formDocumentService,
IDocumentService documentService,
IAvService avService)
IAvService avService,
IOptionsMonitor<AuthClientOptions> options)
: base(user, logger)
{
this._documentGenerationRepository = documentGenerationRepository;
this.avService = avService;

this.keycloakOptions = options;
this._formDocumentService = formDocumentService;
this._documentService = documentService;
}
Expand All @@ -49,7 +53,7 @@ public async Task<ExternalResponse<FileTypes>> GetSupportedFileTypes()
{
this.Logger.LogInformation("Getting supported file Types");

this.User.ThrowIfNotAuthorized(Permissions.GenerateDocuments);
this.User.ThrowIfNotAuthorizedOrServiceAccount(Permissions.GenerateDocuments, keycloakOptions);
ExternalResponse<FileTypes> result = await _documentGenerationRepository.TryGetFileTypesAsync();
return result;
}
Expand Down
Loading
Loading