Skip to content
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
1 change: 1 addition & 0 deletions HealthMonitoring.Client/HealthMonitoring.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.12" />
</ItemGroup>
<ItemGroup>
<None Include="README.md" Pack="true" PackagePath="" />
Expand Down
21 changes: 18 additions & 3 deletions HealthMonitoring.Client/HealthMonitoringRegistrationClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace HealthMonitoring.Client;

Expand Down Expand Up @@ -38,7 +39,9 @@ public interface IHealthMonitoringRegistrationClient
Task<ServiceRegistrationResult> RegisterAndConfigureAsync(ServiceRegistrationOptions options, IEnumerable<DependencyMappingOptions> mappings, CancellationToken cancellationToken = default);
}

public sealed class HealthMonitoringRegistrationClient(HttpClient httpClient) : IHealthMonitoringRegistrationClient
public sealed class HealthMonitoringRegistrationClient(
HttpClient httpClient,
ILogger<HealthMonitoringRegistrationClient>? logger = null) : IHealthMonitoringRegistrationClient
Comment thread
StuartFerguson marked this conversation as resolved.
Resolved
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

Expand All @@ -47,7 +50,7 @@ public async Task<ServiceRegistrationResult> RegisterAsync(ServiceRegistrationOp
ArgumentNullException.ThrowIfNull(options);
ValidateServiceOptions(options);

using var response = await httpClient.PostAsJsonAsync("api/services/register", new ServiceRegistrationPayload(
var payload = new ServiceRegistrationPayload(
options.ServiceId,
options.Name,
options.HealthUrl.ToString(),
Expand All @@ -61,7 +64,19 @@ public async Task<ServiceRegistrationResult> RegisterAsync(ServiceRegistrationOp
options.Group,
options.Description,
options.Version,
options.Host), JsonOptions, cancellationToken);
options.Host);

var loggedPayload = payload with
{
ConnectionString = string.IsNullOrWhiteSpace(payload.ConnectionString)
? payload.ConnectionString
: "[REDACTED]"
};
logger?.LogInformation(
"POST /api/services/register body: {RequestBody}",
JsonSerializer.Serialize(loggedPayload, JsonOptions));

using var response = await httpClient.PostAsJsonAsync("api/services/register", payload, JsonOptions, cancellationToken);

await EnsureSuccessAsync(response, "register service", cancellationToken);
var result = await response.Content.ReadFromJsonAsync<ServiceRegistrationResponsePayload>(JsonOptions, cancellationToken)
Expand Down
23 changes: 23 additions & 0 deletions HealthMonitoring.Tests/Api/ServiceRegistrationLoggingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using HealthMonitoring.Api;
using HealthMonitoring.Domain;

namespace HealthMonitoring.Tests.Api;

public sealed class ServiceRegistrationLoggingTests
{
[Fact]
public void Registration_body_for_logging_redacts_connection_strings()
{
var request = new ServiceRegistrationRequest("orders-db", "Orders DB", "https://orders/health")
{
MonitorType = MonitorType.SqlServer,
ConnectionString = "Server=orders;Database=Orders;Authentication=Integrated Security"
};

var body = ServiceRegistrationLogFormatter.Serialize(request);

Assert.Contains("orders-db", body);
Assert.Contains("[REDACTED]", body);
Assert.DoesNotContain("Integrated Security", body);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Net;
using System.Net.Http.Json;
using HealthMonitoring.Client;
using Microsoft.Extensions.Logging;

namespace HealthMonitoring.Tests.Client;

Expand Down Expand Up @@ -47,6 +48,37 @@ public async Task RegisterAsync_posts_service_configuration_and_returns_acknowle
Assert.True(payload.IgnoreCertificateErrors);
}

[Fact]
public async Task RegisterAsync_logs_the_post_body_without_exposing_connection_strings()
{
var handler = new RecordingHandler(_ => new HttpResponseMessage(HttpStatusCode.Created)
{
Content = JsonContent.Create(new
{
serviceId = PaymentsApiServiceId,
action = "created",
service = new { id = Guid.Parse("11111111-1111-1111-1111-111111111111") }
})
});
var logger = new RecordingLogger<HealthMonitoringRegistrationClient>();
var client = new HealthMonitoringRegistrationClient(
new HttpClient(handler) { BaseAddress = new Uri(MonitoringBaseUrl) },
logger);

await client.RegisterAsync(new ServiceRegistrationOptions
{
ServiceId = PaymentsApiServiceId,
Name = "Payments API",
HealthUrl = new Uri("https://payments-api/health"),
ConnectionString = "Server=secret;Password=super-secret"
});

Assert.Contains("payments-api", logger.Messages.Single());
Assert.Contains("/api/services/register", logger.Messages.Single());
Assert.Contains("REDACTED", logger.Messages.Single());
Assert.DoesNotContain("super-secret", logger.Messages.Single());
}

[Fact]
public async Task SetDependencyMappingsAsync_resolves_target_service_and_upserts_mapping()
{
Expand Down Expand Up @@ -133,6 +165,26 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
}
}

private sealed class RecordingLogger<T> : ILogger<T>
{
public List<string> Messages { get; } = [];

public IDisposable? BeginScope<TState>(TState state) where TState : notnull
{
_ = state;
return null;
}

public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_ = logLevel;
_ = eventId;
Messages.Add(formatter(state, exception));
}
}

private static HttpResponseMessage JsonResponse(HttpStatusCode statusCode, object value) => new(statusCode)
{
Content = JsonContent.Create(value)
Expand Down
34 changes: 33 additions & 1 deletion HealthMonitoring/Api/ServiceRegistrationEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using HealthMonitoring.Domain;
using HealthMonitoring.Persistence;
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace HealthMonitoring.Api;

Expand All @@ -17,8 +19,12 @@ public static IEndpointRouteBuilder MapServiceRegistrationEndpoints(this IEndpoi
}

private static void MapRegistrationEndpoint(IEndpointRouteBuilder endpoints, string dashboardEnvironment) =>
endpoints.MapPost("/api/services/register", async (ServiceRegistrationRequest request, IHealthMonitoringRepository repository, CancellationToken cancellationToken) =>
endpoints.MapPost("/api/services/register", async (ServiceRegistrationRequest request, IHealthMonitoringRepository repository, ILoggerFactory loggerFactory, CancellationToken cancellationToken) =>
{
loggerFactory.CreateLogger("HealthMonitoring.Api.ServiceRegistrationEndpoints").LogInformation(
"POST /api/services/register body: {RequestBody}",
ServiceRegistrationLogFormatter.Serialize(request));

var errors = ServiceConfigurationValidator.Validate(request);
if (errors.Count > 0) return Results.ValidationProblem(errors.ToDictionary(error => error, error => new[] { error }));

Expand Down Expand Up @@ -72,3 +78,29 @@ private static void MapArchiveEndpoint(IEndpointRouteBuilder endpoints) =>
return Results.NoContent();
});
}

public static class ServiceRegistrationLogFormatter
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

public static string Serialize(ServiceRegistrationRequest request) =>
JsonSerializer.Serialize(new
{
request.ServiceId,
request.Name,
request.HealthUrl,
Comment thread
StuartFerguson marked this conversation as resolved.
Resolved
request.MonitorType,
ConnectionString = string.IsNullOrWhiteSpace(request.ConnectionString)
? request.ConnectionString
: "[REDACTED]",
request.IgnoreCertificateErrors,
request.PollingIntervalSeconds,
request.RequestTimeoutSeconds,
request.RetentionDays,
request.StatusPolicy,
request.Group,
request.Description,
request.Version,
request.Host
}, JsonOptions);
}
3 changes: 2 additions & 1 deletion HealthMonitoring/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning"
}
},
"Initialization": {
Expand Down
Loading